Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 107 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Ask in natural language — mindmark remembers what you saved.
| `mindmark validate` | Check indexed bookmark URLs for stale links (HTTP 4xx/5xx or unreachable) and report them |
| `mindmark drop-index` | Delete the local SQLite index database (with confirmation unless `--yes`) |

Human output is concise and TTY-aware: color is enabled in real terminals, disabled automatically for pipes/CI, and can always be turned off with `--no-color`.

> 🔌 **Works offline** after the first run. Embeddings run on-device via [fastembed](https://github.com/qdrant/fastembed) (ONNX Runtime, ~130 MB one-time model download).

### Supported Browsers
Expand Down Expand Up @@ -155,12 +157,16 @@ mindmark sync --list-browsers

Example output:

```
Browser Profile Path
------- ------- ----
Chrome Default ~/Library/.../Google/Chrome/Default/Bookmarks
Chrome Profile 3 ~/Library/.../Google/Chrome/Profile 3/Bookmarks
Edge Default ~/Library/.../Microsoft Edge/Default/Bookmarks
```text
Supported browsers
- Chrome
- Edge
- Brave
- Firefox

Detected profiles
- Chrome (Default) → ~/Library/Application Support/Google/Chrome/Default/Bookmarks
- Edge (Default) → C:\Users\you\AppData\Local\Microsoft\Edge\User Data\Default\Bookmarks
```

</details>
Expand Down Expand Up @@ -238,7 +244,7 @@ mm open "docker setup"

### 4️⃣ JSON output for scripting

Pipe results into **fzf**, **jq**, **Alfred**, **Raycast**, **PowerToys Run**, or any tool that accepts JSON:
Pipe results into **fzf**, **jq**, **Alfred**, **Raycast**, **PowerToys Run**, or any tool that accepts JSON. `find --json` returns the same result object shape as the CLI uses internally:

```bash
# macOS / Linux
Expand All @@ -248,10 +254,44 @@ mindmark find "istio service mesh" --json | jq '.[].url'
mindmark find "istio service mesh" --json | ConvertFrom-Json | ForEach-Object { $_.url }
```

```json
[
{
"score": 0.842,
"title": "Istio / Service Mesh",
"url": "https://istio.io/latest/docs/",
"folder_path": "Work/Kubernetes",
"domain": "istio.io"
}
]
```

If you add `--excerpt`, results that have enriched page content also include `relevant_excerpt`.

---

## 📖 Usage

### Output modes

By default, mindmark prints professional human-readable output with status symbols, hints, and color when stdout is an interactive terminal:

```text
→ Reading bookmarks from Chrome (Default), Firefox (default-release)
✓ Collected 812 bookmarks from 2 profile(s)
→ Syncing index at ~/.mindmark/index.db
✓ Sync complete: added=12, updated=3, removed=0, unchanged=797
Hint: Run 'mindmark find "your query"' to search your bookmarks.
```

Use `--no-color` when you want plain text even in a TTY. `NO_COLOR=1` and `MINDMARK_NO_COLOR=1` are also respected.

```bash
mindmark --no-color stats
```

Use `--json` for stable machine-readable output from `find`, `sync`, `stats`, `validate`, and `enrich`.

### Syncing

`mindmark sync` reads bookmarks directly from your browser data directories. It's **incremental** — only new or changed bookmarks are re-embedded, making re-syncs near-instant.
Expand All @@ -261,12 +301,50 @@ mindmark sync # sync all detected browsers
mindmark sync --browser chrome # sync only Chrome
mindmark sync --browser firefox # sync only Firefox
mindmark sync --list-browsers # list detected browsers and profiles
mindmark sync --json # emit sync summary as JSON
```

When you add new bookmarks in your browser, just run `mindmark sync` again — it will pick up only the changes.

> 💡 **Note:** If you change the embedding model with `--model`, all bookmarks will be re-embedded on the next sync. Browser names are case-insensitive (e.g., `--browser Chrome` and `--browser chrome` both work).

`sync --json` returns a top-level `summary`, synced `profiles`, any `warnings`, plus `db_path` and `model`.

### Stats

```bash
mindmark stats
mindmark stats --json
```

Example human output:

```text
Bookmarks: 812
Index: ~/.mindmark/index.db
Model: BAAI/bge-small-en-v1.5

Top domains
github.com: 42
docs.python.org: 18

Top folders
Work/Kubernetes: 27
Reading: 14
```

`stats --json` returns:

```json
{
"db_path": "/home/you/.mindmark/index.db",
"model": "BAAI/bge-small-en-v1.5",
"top_domains": [{"count": 42, "domain": "github.com"}],
"top_folders": [{"count": 27, "folder": "Work/Kubernetes"}],
"total": 812
}
```

### Filters and options

Narrow down results without changing your query:
Expand All @@ -293,7 +371,7 @@ Use `drop-index` to remove the local SQLite index database when you want a clean
```bash
mindmark drop-index # asks for confirmation
mindmark drop-index --yes # skip confirmation
mindmark drop-index --db /path/to/index.db
mindmark --db /path/to/index.db drop-index
```

### Validate stale links
Expand All @@ -304,9 +382,10 @@ Use `validate` to probe all indexed HTTP(S) bookmark URLs and identify stale one
mindmark validate # identify all stale bookmarks
mindmark validate --timeout 5 # per-request timeout in seconds (default 8)
mindmark validate --workers 32 # parallel URL checks (default 16)
mindmark validate --json # emit validation summary as JSON
```

Non-HTTP URLs (for example `file:` or browser-internal URLs) are skipped and not checked.
Non-HTTP URLs (for example `file:` or browser-internal URLs) are skipped and not checked. `validate --json` returns `total`, `checked`, `healthy`, `skipped`, `stale_count`, and a `stale` array with `title`, `url`, `folder_path`, `status_code`, `reason`, and `error`.

### Swap the embedding model

Expand Down Expand Up @@ -367,6 +446,7 @@ Without enrichment, searching for **"authentication strategies"** on a bookmark

```bash
mindmark enrich --limit 100 --workers 4
mindmark enrich --limit 100 --workers 4 --json
```

Options:
Expand Down Expand Up @@ -408,22 +488,28 @@ The `⤵` symbol indicates content from the enriched page. Without enrichment, t

### Status and monitoring

Check enrichment status:
Get a machine-readable enrichment run summary:

```bash
python -c "
from mindmark.index import Index
idx = Index()
print(idx.enrichment_stats())
idx.close()
"
mindmark enrich --json
```

Example output:
```python
{'pending': 1234, 'complete': 450, 'failed': 23}
```json
{
"before": {"pending": 1234, "complete": 450, "failed": 23},
"after": {"pending": 1134, "complete": 550, "failed": 25},
"complete": 100,
"failed": 2,
"reset_failed": 0,
"skipped": 0,
"status": "complete",
"total": 102
}
```

> `mindmark enrich --json` still performs enrichment when work is pending. To inspect counts without fetching pages, use the Python API (`Index().enrichment_stats()`).

### Notes

- **100% local** — Page fetching happens on your machine; no cloud service is used.
Expand All @@ -433,9 +519,11 @@ Example output:

---

## 💾 Storage Layout

| What | macOS / Linux | Windows | Override |
|---|---|---|---|
| Index database | `~/.mindmark/index.db` | `%LOCALAPPDATA%\mindmark\index.db` | `--db` flag or `MINDMARK_DB` env var |
| Index database | `~/.mindmark/index.db` | `%LOCALAPPDATA%\mindmark\index.db` | global `--db` flag (before the command) or `MINDMARK_DB` env var |
| Home directory | `~/.mindmark/` | `%LOCALAPPDATA%\mindmark\` | `MINDMARK_HOME` env var |
| Embedding model | `~/.cache/fastembed/` | `%LOCALAPPDATA%\fastembed\` | Managed by fastembed |

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mindmark"
version = "0.1.6"
version = "0.1.7"
description = "Local semantic search over your browser bookmarks — on-device embeddings, no cloud."
readme = "README.md"
requires-python = ">=3.9"
Expand Down
2 changes: 1 addition & 1 deletion src/mindmark/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""mindmark — local semantic search over your browser bookmarks."""
__version__ = "0.1.0"
__version__ = "0.1.6"
72 changes: 72 additions & 0 deletions src/mindmark/_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Small TTY-aware console formatting helpers."""
from __future__ import annotations

import os
import sys
from typing import TextIO

_TRUTHY = {"1", "true", "yes", "on"}
_COLORS = {
"muted": "2",
"status": "36",
"success": "32",
"warning": "33",
"error": "31",
"accent": "35",
"bold": "1",
}


def _env_disables_color() -> bool:
return (
"NO_COLOR" in os.environ
or os.environ.get("MINDMARK_NO_COLOR", "").lower() in _TRUTHY
or os.environ.get("TERM") == "dumb"
)


class Console:
def __init__(
self,
*,
color: bool | None = None,
stdout: TextIO | None = None,
stderr: TextIO | None = None,
) -> None:
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
if color is None:
self.color = self.stdout.isatty() and not _env_disables_color()
else:
self.color = color and not _env_disables_color()

def style(self, text: str, name: str) -> str:
code = _COLORS.get(name)
if not self.color or not code:
return text
return f"\033[{code}m{text}\033[0m"

def out(self, message: str = "", *, style: str | None = None) -> None:
print(self.style(message, style) if style else message, file=self.stdout)

def err(self, message: str = "", *, style: str | None = None) -> None:
print(self.style(message, style) if style else message, file=self.stderr)

def status(self, message: str) -> None:
self.out(f"{self.style('→', 'status')} {message}")

def success(self, message: str) -> None:
self.out(f"{self.style('✓', 'success')} {message}")

def warning(self, message: str) -> None:
self.err(f"{self.style('!', 'warning')} {message}")

def error(self, message: str) -> None:
self.err(f"{self.style('✖', 'error')} {message}")

def hint(self, message: str, *, stderr: bool = False) -> None:
line = f"{self.style('Hint:', 'accent')} {message}"
if stderr:
self.err(line)
else:
self.out(line)
8 changes: 3 additions & 5 deletions src/mindmark/browsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
"""
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
import json
import sqlite3

from ..parser import Bookmark
from ..index import SyncResult
from .paths import detect_browsers, BrowserProfile, SUPPORTED_BROWSERS


Expand All @@ -37,8 +34,9 @@ def collect_all_bookmarks(
"""
profiles = detect_browsers()
if browser_filter:
filt = browser_filter.lower()
profiles = [p for p in profiles if p.browser_name.lower() == filt]
filt = browser_filter.strip().lower()
if filt:
profiles = [p for p in profiles if p.browser_name.lower() == filt]

results: list[tuple[BrowserProfile, list[Bookmark]]] = []
for profile in profiles:
Expand Down
Loading
Loading