Single binary. Zero cloud. Point it at a folder of .md files and it indexes them into a vector database. Query with natural language. Everything runs locally with ONNX embeddings — no API keys, no uploads.
cargo install --path .Or build from source:
# Replace <your-fork> once a remote is configured; the repo is local-only until then.
git clone https://github.com/<your-fork>/tv
cd tv
cargo build --releasePre-built binary lands at target/release/tv (or tv.exe on Windows).
# Point at your Obsidian vault (or any folder of .md files)
export TV_VAULT=~/my-vault
# Index everything
tv reindex
# Ask a question
tv query "what did I write about shipping costs"
# Start the watch daemon (auto-reindexes on file changes)
tv watch
# Check daemon health
tv status| Command | What it does |
|---|---|
tv reindex |
Full rebuild of the index from scratch |
tv query "text" |
One-shot semantic search over the vault |
tv watch |
Start daemon — watches files, auto-indexes changes |
tv status |
Read and display status.json |
- Chunk — Splits markdown into heading-aware paragraphs (~1000 chars with 150-char overlap)
- Embed — Converts each chunk to a 384-dim vector using
bge-small-en-v1.5(quantized ONNX) - Index — Stores vectors in a
turbovecquantized index (.tvimfile) + SQLite metadata sidecar - Search — Embeds your query, finds nearest neighbors, ranks by cosine similarity
All model files are cached locally in TV_MODEL_CACHE. First run downloads ~70 MB.
All knobs are env vars — copy .env.example to .env and tweak:
| Env var | Default | What it controls |
|---|---|---|
TV_VAULT |
./vault |
Path to your markdown vault |
TV_INDEX_BASE |
./index/obsidian |
Stem for .tvim / .sqlite / status.json |
TV_MODEL_CACHE |
./index/models |
ONNX model cache directory |
TV_DIM |
384 |
Embedding dimensions |
TV_BIT_WIDTH |
4 |
Vector index quantization bits |
TV_CHUNK_CHARS |
1000 |
Characters per chunk |
TV_CHUNK_OVERLAP |
150 |
Overlap between adjacent chunks |
TV_DEBOUNCE |
1.5 |
Filesystem debounce (seconds) |
TV_TOPK |
5 |
Results per query |
TV_PER_FILE_CAP |
2 |
Max results from a single file |
TV_DEPRIORITIZE_PATH |
(empty) | Path prefix to deprioritize in results |
TV_DEPRIORITIZE_PENALTY |
0.9 |
Score multiplier for deprioritized paths |
TV_IDLE_UNLOAD_SEC |
120 |
Unload ONNX model after idle (0 = never) |
TV_EMBED_THREADS |
2 |
ONNX inference threads |
TV_MAX_FILE_BYTES |
1048576 |
Skip files larger than this |
TV_HTTP_PORT |
(off) | Enable HTTP status endpoint on watch |
TV_WATCH_EXIT_AFTER_SEC |
(off) | Auto-stop daemon after N seconds |
When tv watch is running with TV_HTTP_PORT=9127, you get a live health endpoint:
curl http://localhost:9127/
# {"pid":12345,"state":"watching","files":977,"chunks":5571,"vectors":5570,...}Without HTTP, health is written to status.json on every heartbeat (checkable with tv status).
.obsidian,.trash,.git, and.turbovecdirectories are excluded from indexing- Only
.mdfiles are indexed - First run downloads the BGE ONNX model (~70 MB) — cached forever after
- The daemon uses a lock file to prevent duplicate instances
- Stale lock files from crashed processes are detected and cleaned automatically
- Cross-platform: tested on Windows (MSVC) and Linux. Atomic index writes use
MoveFileExWon Windows,rename()elsewhere.
Source-available with a commercial royalty — see LICENSE. Non-commercial use is free; any profit-making use requires a written royalty agreement with the author. Authorship is anchored by a dated cryptographic provenance record (PROVENANCE.md). Changelog in CHANGELOG.md.