# freshvault [![CI](https://github.com/KIOKO-LAB/freshvault/actions/workflows/ci.yml/badge.svg)](https://github.com/KIOKO-LAB/freshvault/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/freshvault)](https://www.npmjs.com/package/freshvault) [![MCP Registry](https://img.shields.io/badge/MCP_registry-io.github.KIOKO--LAB%2Ffreshvault-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=freshvault) ![node](https://img.shields.io/node/v/freshvault) [한국어](./README.ko.md) · [日本語](./README.ja.md) **Your Obsidian vault is Claude's memory. Always fresh, always local.** Every other vault-search MCP makes you re-run an index command, babysit a watch terminal, or click "Update Index" in a web UI. freshvault watches your vault from *inside* the MCP server: edit a note, and Claude sees it seconds later. Automatically. Forever. ![freshvault demo: save a note, the watcher reindexes it automatically, semantic search finds it seconds later](https://raw.githubusercontent.com/KIOKO-LAB/freshvault/main/docs/demo.gif) - 🔄 **Never reindex** — the file watcher lives in the server process; boot catch-up absorbs offline edits - 🔒 **100% local** — embeddings via Ollama (`bge-m3`), your notes never leave your machine - 🌏 **Multilingual by default** — `bge-m3` handles Korean, Japanese, and 100+ languages that English-only defaults fail on - 🎯 **Benchmark-driven retrieval** — unconditional BM25 fusion wrecked Korean paraphrase queries (82.5% → 47.5% top-1), so it's off; pure dense missed identifier queries outright (57.1%), so it's not gone either. Lexical scoring is **gated on code-like query tokens**: paraphrase 82.5% → 82.5% (identical), identifiers 57.1% → 100% — [receipts](docs/ko-bench.md) - 🪶 **No vector DB, no Docker, no Python** — JSON metadata + a Float32 sidecar, plain Node, source you can read in one sitting - 🧠 **Chunking that respects sentences** — YAML frontmatter stripped, splits on paragraph/sentence boundaries (CJK-aware) ## Install Prerequisites: [Node 20+](https://nodejs.org) and [Ollama](https://ollama.com). ```bash npx -y freshvault setup ``` That's it. The wizard detects your Obsidian vault, pulls the embedding model, builds the index, and registers with Claude Code. There is no step 2, and there is never a step 2: no `index` command to re-run, no `watch` terminal, no background service.
Manual install (Claude Desktop / Cursor / Windsurf) ```json { "mcpServers": { "freshvault": { "command": "npx", "args": ["-y", "freshvault", "serve"], "env": { "FRESHVAULT_VAULT": "/absolute/path/to/your/vault" } } } } ``` Claude Code one-liner: ```bash claude mcp add freshvault -s user -- npx -y freshvault serve ```
## Use Just ask Claude about your notes: > "Search my notes for what I wrote about cache eviction strategies" Three tools, all read-only: | tool | what it does | |---|---| | `search_notes` | semantic search + optional `folder` / `tags` / `modified_after` / `modified_before` scoping; exact-title lookups boosted; at most 2 chunks per note so one long note can't own the results (`max_per_file`) | | `get_note_context` | full note + its backlinks/outlinks from the vault link graph (path-traversal safe) | | `index_status` | freshness report: notes/chunks, excluded count, last sync, watcher state | Scoped queries competitors gate behind settings or paywalls work per-query here: > "Search my notes tagged #project modified after June for the budget discussion" ## How it works ``` Obsidian vault ──fs.watch──▶ freshvault MCP server ──search_notes──▶ Claude (.md files) (chunks → bge-m3 embeddings (generation) → one JSON index, incremental) ``` - **Incremental**: only changed/deleted notes are re-embedded (mtime+size diff), debounced 4s - **Safety net**: a 60s mtime sweep catches events the watcher misses (network drives, atomic-rename editors) - **Multi-client safe**: first server process becomes the writer (heartbeated lock); others are readers that hot-reload and promote themselves if the writer dies - **Transactional**: an embedding-server outage mid-index can never lose or corrupt notes - **Scale**: vectors live in a packed Float32 sidecar (fast startup, compact); brute-force cosine over thousands of chunks is milliseconds. Honest note: search is still linear — sub-100ms into tens of thousands of chunks, but this is not a vector DB replacement for huge corpora ### Multiple vaults Register one server per vault — index files are kept per-vault automatically: ```bash claude mcp add work-vault -s user -e FRESHVAULT_VAULT=/path/to/work -- npx -y freshvault serve claude mcp add personal-vault -s user -e FRESHVAULT_VAULT=/path/to/personal -- npx -y freshvault serve ``` ### Other embedding servers (LM Studio, LiteLLM, OpenAI-compatible) ```bash FRESHVAULT_EMBED_API=openai FRESHVAULT_EMBED_URL=http://localhost:1234 npx -y freshvault serve ``` Anything speaking `/v1/embeddings` works; `FRESHVAULT_EMBED_KEY` for authenticated endpoints (never written to the config file). ## Configuration Everything works with zero config after `setup`. Override when needed: | Flag | Env | Default | |---|---|---| | `--vault` | `FRESHVAULT_VAULT` | from `setup` | | `--model` | `FRESHVAULT_MODEL` | `bge-m3` | | `--ollama-url` | `FRESHVAULT_OLLAMA_URL` | `http://localhost:11434` | | `--data` | `FRESHVAULT_DATA` | platform data dir | | — | `FRESHVAULT_EMBED_API` | `ollama` (or `openai`) | | — | `FRESHVAULT_EMBED_URL` | `http://localhost:1234` (openai mode) | | — | `FRESHVAULT_EMBED_KEY` | none (openai mode, optional) | | — | `FRESHVAULT_IGNORE` | none — e.g. `Templates/,Daily/**` (or `ignore: []` in config) | | — | `FRESHVAULT_LEXICAL_GATE` | `on` — lexical scoring for code-like query tokens; `off` for pure dense (or `lexicalGate: false` in config) | Commands: `setup` · `serve` (default) · `index` (manual escape hatch) · `status` ### Ignore patterns Patterns follow **gitignore** semantics, matched against vault-relative paths: | Pattern | Excludes | |---|---| | `Templates/` | any `Templates` directory at any depth — `Templates/`, `work/Templates/` | | `/Templates` | only the vault-root `Templates/` | | `Daily/**` | everything under the **root-level** `Daily/` only — the internal slash anchors it | | `**/Daily/**` | everything under any `Daily/`, at any depth | | `*.excalidraw.md` | that filename at any depth | | `work/wip.md` | exactly that path, relative to the vault root | - A slash **anywhere but the end** anchors the pattern to the vault root. A trailing slash only means "directory" — it does **not** anchor. - `*` and `?` never cross a `/`. `**` crosses directories only as a whole path segment (`**/x`, `x/**`, `x/**/y`); glued inside a segment it collapses to a single `*`, as in git. - Matches end on a segment boundary — `Templates` never matches `TemplatesOld`. - `!` negation and character classes are **not** supported; such patterns match nothing. - `FRESHVAULT_IGNORE` is comma-separated, so a pattern cannot contain a comma. > **Behaviour change since v0.3.0.** v0.3.0 treated a *trailing* slash as a root anchor, so `Templates/` excluded only the vault-root `Templates/` and silently indexed every nested one — no warning, just a quietly polluted index. Patterns shaped like `Templates/`, `/Templates`, `**/x` and `a/**/b` now exclude **more** than before (`/Templates` previously matched nothing at all). Conversely, `**` glued inside a segment (`Daily**md`) no longer crosses `/`. Run `freshvault status` after upgrading and compare the excluded count if it matters to you. ## Benchmark Two retrieval micro-benchmarks ship in-repo (`node scripts/bench.mjs`): **ko-bench** (30 Korean notes, 40 paraphrase queries) and **id-bench** (38 technical notes, 14 identifier queries). Both run under three scoring modes — dense, hybrid-always, and the shipped gated mode — and the script checks the no-regression contract itself. Results and the bge-m3-ko (85.0% top-1, 634MB) import guide in [docs/ko-bench.md](docs/ko-bench.md). ## Roadmap - MCPB bundle for one-click Claude Desktop install - Reranking pass for large vaults - PDF text extraction at index time ## License MIT © Kioko Lab