Skip to main content
Glama
Reppin123

thought-search

by Reppin123

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, 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 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 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 is the battle-tested routing doctrine to put in your agent's system prompt.

Install

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, 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)

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 into your system prompt — the tools are half the product; the routing doctrine is the other half.

As a library

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.mp3talk.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 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), here reimplemented local-first. Embeddings by all-MiniLM-L6-v2 over ONNX Runtime. Extracted from Apprentice.

MIT.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Reppin123/thought-search'

If you have feedback or need assistance with the MCP directory API, please join our Discord server