Skip to main content
Glama
Reppin123

thought-search

by Reppin123
README.md
# thought-search

**Find any file on your Mac by a vague memory of it.** Spotlight for names, a
local semantic index for meaning, and results shaped for agents: a `path:line`
to start reading from, not a chunk dump. Nothing leaves the machine.

```
$ thought grep "the essay about calculating the punch force that could snap a jaw"
~/Documents/college/fermi-estimates-draft.pdf:118 (0.68) — assume the fist decelerates over ~2cm of jaw...

$ thought find "financial memo" --kind presentation
Found 2 in your work folders (newest first):
1. ~/Documents/Acme Financial - One Page Memo.pptx — 3d ago — 4.2MB
2. ~/Downloads/memo-draft-old.pptx — 2mo ago — 3.9MB
```

This is the file-search layer of [Apprentice](https://withapprentice.com), a
macOS AI apprentice, extracted as a standalone tool. It shipped to real users
first; the code here is the shipped code, war-story comments included.

## The problem

You know the file exists. You wrote it. You just can't produce the one thing
every search interface demands: its name.

Two tools already live on your Mac, and each is broken alone:

- **Spotlight** is fast and literal. It finds `fermi-estimates-draft.pdf` if
  you type "fermi". You didn't remember "fermi". You remembered *punch force*
  and *jaw*.
- **An agent with shell access** can `ls`, `cat`, and `grep` its way around —
  powerful, but blind. Every wrong folder it opens burns seconds and context
  window, and an unscoped `mdfind` or `find /` is a 30-60 second walk.

Semantic search should fix this, and in RAG form it half does: one query pulls
content by meaning. But it hands back top-K chunks ripped out of their files —
an answer fragment with no neighbors, no thread to pull, nowhere to stand.

The whole point is: you shouldn't have to pick.

## The bet

Supermemory's [SMFS](https://smfs.ai/research/memory-as-a-filesystem/) work
put numbers on the right design: let semantic search land on a **path**, then
let the agent read, grep, and reason *from that path* with the tools it
already has. Reach of semantic search, control of agentic search, one motion.
([Their write-up](https://supermemory.ai/blog/smfs-making-agentic-retrieval-55-cheaper-and-more-accurate)
is worth your time.)

SMFS mounts a cloud memory backend as a filesystem, so half their system is a
sync engine: push queues, delta pulls, watermarks, FUSE/NFS mounts.

We took the other half of the idea and inverted the premise: **your Mac is
already the filesystem.** The files are already local, already canonical,
already yours. What's missing is not a mount — it's the semantic layer over
what's there. So thought-search is the retrieval model without the cloud:
no daemon, no sync, no account. An index in SQLite, a matrix in RAM, and two
primitives.

## Two primitives

**`find_file`** — the lexical leg. Name + kind + time + folder, straight off
the OS's own Spotlight index (`mdfind`, always scoped with `-onlyin`, never a
directory walk). Multi-word queries are tokenized so "financial memo" matches
`Acme Financial - One Page Memo.pptx`. Temporal queries use
`kMDItemLastUsedDate` — when you last *opened* the file, not when some
background process touched it — so "the deck I worked on yesterday" means what
you meant. On a miss it escalates: work folders → whole home directory →
external drives. It never dead-ends silently; it tells you where it looked.

**`semantic_grep`** — the semantic leg. File *contents*, chunked and embedded
locally (ONNX MiniLM, no PyTorch, ~15MB of runtime), searched by cosine over
an in-RAM matrix. A hit is `path:line (score) — excerpt`: a launch point. Open
the file, read around the line, follow the thread.

The router between them is not code — it's the model. `find_file` for
name/metadata references, `semantic_grep` for content references, and the
agent picks per query, exactly the way you reach for `grep` vs `grep -r`.
[docs/AGENT_PROMPT.md](docs/AGENT_PROMPT.md) is the battle-tested routing
doctrine to put in your agent's system prompt.

## Install

```bash
pip install git+https://github.com/Reppin123/thought-search.git

thought index            # crawl Downloads/Desktop/Documents, embed locally
thought grep "that doc about our pricing model"
thought find "contract" --since 'last week'
thought status
```

The embedding model (~90MB, [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2),
Apache-2.0) downloads once on first use. After that the tool runs fully
offline. macOS only — the lexical leg *is* Spotlight.

### For agents (MCP)

```bash
pip install "thought-search[mcp]"
claude mcp add thought-search -- thought-mcp
```

Any MCP client works: `{"mcpServers": {"thought-search": {"command": "thought-mcp"}}}`.
Then wire [docs/AGENT_PROMPT.md](docs/AGENT_PROMPT.md) into your system
prompt — the tools are half the product; the routing doctrine is the other
half.

### As a library

```python
import asyncio
from thought_search import find_files, index_paths, search, start_background_index

index_paths(["~/Documents"])                  # foreground crawl
start_background_index()                      # or: polite daemon-thread crawl
hits = search("unit economics assumptions")   # [{path, score, snippet, start_line}]
res = asyncio.run(find_files("financial memo", kind="presentation"))
```

## What makes it fast (and keeps it fast)

Everything below was a measured failure first. The numbers come from the
machine this shipped on — a real corpus that grew to **7,207 files / 280k
chunks / 1.2GB of index** — not a synthetic benchmark.

**Content-hash freshness.** Every file's text is hashed with a settings
fingerprint (model, dims, chunking params). Identical bytes never re-embed:
unchanged files skip on an mtime+size gate without even being read, and a
moved/renamed/copied file reuses its vectors for free. Steady-state re-crawls
cost approximately nothing.

**Progressive commits.** The index commits every 20 embedded files. The first
version committed at the end of the crawl — 26 minutes in, the WAL held 71MB
and every search said "index empty." Partial results should be searchable
*during* the first crawl, and a killed process should keep its progress.

**A polite background crawl.** The first cut pegged ~4 cores for 26 minutes.
Now: `os.nice(10)` plus a per-embed throttle. An index that makes the laptop
fans spin is an index that gets uninstalled.

**One matrix in RAM.** Brute-force cosine is a single matmul — *if* the
vectors are already in memory. Reloading 388MB from SQLite per query cost
3.5-5.8s; cached, a query is ~40ms at 252k chunks. The cache key is the chunk
*count*, not SQLite's `data_version` — that pragma is per-connection, and
keying on it silently rebuilt the cache on every single query.

**Best-chunk-per-file, lazily.** Results are files, not chunks: argsort once,
walk until top-k distinct files, early-exit. The cache holds no Python
strings — at 252k rows, six 252k-element Python lists were a 73s warm-up cost
by themselves. Snippets are fetched from SQLite only for the winners.

**No type filter on semantic search.** An inferred file-type filter is a
silent excluder — a wrong `kind` guess once dropped the exact PDF the query
was about, and the agent thrashed for 8 turns on a file the index had.
`semantic_grep` returns every type and shows the extension; choosing is the
caller's job.

**bf16 storage: evaluated, rejected.** Halving vector RAM sounds free until
you notice numpy has no BLAS f16 matmul — you pay a 2-3s per-query upcast or
you upcast at load and the RAM comes back. Honest dead ends stay in the
comments so nobody re-walks them.

## What it deliberately doesn't do

- **No cloud, no sync engine, no account.** SMFS needs push queues and delta
  pulls because it mirrors to a backend. There is no backend here. Your files
  never leave your machine; neither do their embeddings.
- **No daemon, no FUSE/NFS mount.** It's a library, a CLI, and an MCP server.
  The OS filesystem is already mounted.
- **No hardcoded query planner.** The model decomposes "the pdf about X from
  last week" into parameters inline. Speed comes from the indexes; smarts stay
  in the model.

## Status and honest limitations

- **macOS only.** The lexical leg is Spotlight. A Linux leg would need
  `locate`/`plocate` or similar.
- **Index freshness is crawl-based** (session start / `thought index`), not a
  live file-watcher. Watcher is the natural next step.
- **Text, code, PDF, and docx** are indexed today. Images/audio/video want
  transcription siblings (`talk.mp3` → `talk.mp3.transcript.md`) — designed,
  not built.
- **MiniLM is the floor.** 384-dim, 256-token context; it lands on the right
  files but scores are modest. The embedder is a deliberate seam — swapping in
  a longer-context model is a two-constant change plus a re-index.
- `start_line` is chunk-start, not an exact span.
- **The eval is an A/B gate, not a benchmark.** [`evals/find_file_ab.py`](evals/find_file_ab.py)
  scores hit@1 / found / latency against a bare-`ls` control on *your* files;
  there is no xAFS-style public corpus here yet. Numbers above are one real
  machine, disclosed as such.

## Credits

The retrieval philosophy — semantic search as a navigation aid that lands on
paths — is SMFS's ([technical report](https://smfs.ai/research/memory-as-a-filesystem/)),
here reimplemented local-first. Embeddings by
[all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
over ONNX Runtime. Extracted from [Apprentice](https://withapprentice.com).

MIT.