context-vault 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,137 +1,43 @@
1
1
  # context-mcp
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@fellanh/context-mcp)](https://www.npmjs.com/package/@fellanh/context-mcp)
4
- [![npm downloads](https://img.shields.io/npm/dm/@fellanh/context-mcp)](https://www.npmjs.com/package/@fellanh/context-mcp)
5
- [![license](https://img.shields.io/npm/l/@fellanh/context-mcp)](./LICENSE)
6
- [![node](https://img.shields.io/node/v/@fellanh/context-mcp)](https://nodejs.org)
3
+ [![npm version](https://img.shields.io/npm/v/context-vault)](https://www.npmjs.com/package/context-vault)
4
+ [![npm downloads](https://img.shields.io/npm/dm/context-vault)](https://www.npmjs.com/package/context-vault)
5
+ [![license](https://img.shields.io/npm/l/context-vault)](./LICENSE)
6
+ [![node](https://img.shields.io/node/v/context-vault)](https://nodejs.org)
7
7
 
8
- Personal context vault an MCP server that connects any AI agent to your accumulated knowledge.
8
+ Persistent memory for AI agents saves and searches knowledge across sessions.
9
9
 
10
- Your knowledge lives as markdown files in plain folders you own and can edit, version, or move freely. The SQLite database is a derived search index — rebuilt from those files at any time. The canonical flow is **files → DB** with a clean two-way mapping: disk structure determines folder placement, the DB stays flat and lightweight.
10
+ <p align="center">
11
+ <img src="assets/demo.gif" alt="context-vault demo — Claude Code and Cursor using the knowledge vault" width="800">
12
+ </p>
11
13
 
12
- ## How It Works
13
-
14
- ```
15
- YOUR FILES (source of truth) SEARCH INDEX (derived)
16
- ~/vault/ ~/.context-mcp/vault.db
17
- ├── insights/ ┌───────────────────────────────┐
18
- │ ├── react-query-caching.md │ vault table │
19
- │ ├── sqlite-fts5-gotchas.md │ kind: insight │
20
- │ └── react/ │ meta.folder: null (flat) │
21
- │ └── hooks/ │ meta.folder: "react/hooks" │
22
- │ └── use-query-gotcha.md │ kind: decision │
23
- ├── decisions/ │ kind: pattern │
24
- │ └── use-sqlite-over-pg.md │ kind: <any custom> │
25
- ├── patterns/ │ + FTS5 full-text │
26
- │ └── api-error-handler.md │ + vec0 embeddings │
27
- └── references/ (custom kind) └───────────────────────────────┘
28
- └── react-19-notes.md
29
- Human-editable, git-versioned Fast hybrid search, RAG-ready
30
- You own these files Rebuilt from files anytime
31
- ```
32
-
33
- The SQLite database is stored at `~/.context-mcp/vault.db` by default (configurable via `--db-path`, `CONTEXT_MCP_DB_PATH`, or `config.json`). It contains FTS5 full-text indexes and sqlite-vec embeddings (384-dim float32, all-MiniLM-L6-v2). The database is a derived index — delete it and the server rebuilds it automatically on next session.
34
-
35
- Requires **Node.js 20** or later.
36
-
37
- ## Install
38
-
39
- ### Quick Start
14
+ ## Quick Start
40
15
 
41
16
  ```bash
42
- curl -fsSL https://raw.githubusercontent.com/fellanH/context-mcp/main/install.sh | sh
43
- ```
44
-
45
- ### npm (Recommended)
46
-
47
- ```bash
48
- npm install -g @fellanh/context-mcp
17
+ npm install -g context-vault
49
18
  context-mcp setup
50
19
  ```
51
20
 
52
- The `setup` command auto-detects installed tools (Claude Code, Claude Desktop, Cursor, Windsurf, Cline), lets you pick which to configure, and writes the correct MCP config for each. Existing configs are preserved only the `context-mcp` entry is added or updated.
53
-
54
- ### Local Development
55
-
56
- ```bash
57
- git clone https://github.com/fellanH/context-mcp.git
58
- cd context-mcp
59
- npm install
60
-
61
- # Interactive setup — detects your tools and configures them
62
- node bin/cli.js setup
63
- ```
64
-
65
- For non-interactive environments (CI, scripts):
66
-
67
- ```bash
68
- context-mcp setup --yes
69
- ```
70
-
71
- ### Manual Configuration
72
-
73
- If you prefer manual setup, add to your tool's MCP config. Pass `--vault-dir` to point at your vault folder (omit it to use the default `~/vault/`).
74
-
75
- **npm install** (portable — survives upgrades):
76
-
77
- ```json
78
- {
79
- "mcpServers": {
80
- "context-mcp": {
81
- "command": "context-mcp",
82
- "args": ["serve", "--vault-dir", "/path/to/vault"]
83
- }
84
- }
85
- }
86
- ```
87
-
88
- **Local dev clone** (absolute path to source):
89
-
90
- ```json
91
- {
92
- "mcpServers": {
93
- "context-mcp": {
94
- "command": "node",
95
- "args": ["/path/to/context-mcp/src/server/index.js", "--vault-dir", "/path/to/vault"]
96
- }
97
- }
98
- }
99
- ```
100
-
101
- You can also pass config via environment variables in the MCP config block:
21
+ Setup auto-detects your tools (Claude Code, Claude Desktop, Cursor, Windsurf, Cline), downloads the embedding model, seeds your vault with a starter entry, and verifies everything works. Then open your AI tool and try:
102
22
 
103
- ```json
104
- {
105
- "mcpServers": {
106
- "context-mcp": {
107
- "command": "context-mcp",
108
- "args": ["serve"],
109
- "env": {
110
- "CONTEXT_MCP_VAULT_DIR": "/path/to/vault"
111
- }
112
- }
113
- }
114
- }
115
- ```
23
+ > "Search my vault for getting started"
116
24
 
117
- ### How the Server Runs
25
+ ## What It Does
118
26
 
119
- The server is an MCP (Model Context Protocol) process — you don't start or stop it manually. Your AI client (Claude Code, Cursor, Cline, etc.) spawns it automatically as a child process when a session begins, based on the `mcpServers` config above. The server communicates over stdio and lives for the duration of the session. When the session ends, the client terminates the process and SQLite cleans up its WAL files.
120
-
121
- This means:
122
- - **No daemon, no port, no background service.** The server only runs while your AI client is active.
123
- - **Multiple sessions** can run separate server instances concurrently — SQLite WAL mode handles concurrent access safely.
124
- - **First launch** downloads the embedding model (~22MB, all-MiniLM-L6-v2) and creates the database. Subsequent starts are fast.
125
- - **Auto-reindex** on first tool call per session ensures the search index is always in sync with your files on disk. No manual reindex needed.
27
+ - **Save** insights, decisions, patterns, and any custom knowledge kind from AI sessions
28
+ - **Search** with hybrid full-text + semantic similarity, ranked by relevance and recency
29
+ - **Own your data** — plain markdown files in folders you control, git-versioned, human-editable
126
30
 
127
31
  ## Tools
128
32
 
129
- The server exposes three tools. Your AI agent calls them automatically — you don't invoke them directly.
33
+ The server exposes five tools. Your AI agent calls them automatically — you don't invoke them directly.
130
34
 
131
35
  | Tool | Type | Description |
132
36
  |------|------|-------------|
133
37
  | `get_context` | Read | Hybrid FTS5 + vector search across all knowledge |
134
- | `save_context` | Write | Save any kind of knowledge to the vault |
38
+ | `save_context` | Write | Save new knowledge or update existing entries by ID |
39
+ | `list_context` | Browse | List vault entries with filtering and pagination |
40
+ | `delete_context` | Delete | Remove an entry by ID (file + index) |
135
41
  | `context_status` | Diag | Show resolved config, health, and per-kind file counts |
136
42
 
137
43
  ### `get_context` — Search your vault
@@ -147,9 +53,10 @@ get_context({
147
53
 
148
54
  Returns entries ranked by combined full-text and semantic similarity, with recency weighting.
149
55
 
150
- ### `save_context` — Save knowledge
56
+ ### `save_context` — Save or update knowledge
151
57
 
152
58
  ```js
59
+ // Create new entry
153
60
  save_context({
154
61
  kind: "insight", // Determines folder: insights/
155
62
  body: "React Query staleTime defaults to 0",
@@ -159,11 +66,44 @@ save_context({
159
66
  folder: "react/hooks", // Optional: subfolder organization
160
67
  source: "debugging-session" // Optional: provenance
161
68
  })
162
- // → ~/vault/insights/react/hooks/staletime-gotcha.md
69
+ // → ~/vault/knowledge/insights/react/hooks/staletime-gotcha.md
70
+
71
+ // Update existing entry by ID
72
+ save_context({
73
+ id: "01HXYZ...", // ULID from a previous save
74
+ body: "Updated content here", // Only provide fields you want to change
75
+ tags: ["react", "updated"] // Omitted fields are preserved
76
+ })
163
77
  ```
164
78
 
165
79
  The `kind` field accepts any string — `"insight"`, `"decision"`, `"pattern"`, `"reference"`, or any custom kind. The folder is auto-created from the pluralized kind name.
166
80
 
81
+ When updating (`id` provided), omitted fields are preserved from the original. You cannot change `kind` or `identity_key` — delete and re-create instead.
82
+
83
+ ### `list_context` — Browse entries
84
+
85
+ ```js
86
+ list_context({
87
+ kind: "insight", // Optional: filter by kind
88
+ category: "knowledge", // Optional: knowledge, entity, or event
89
+ tags: ["react"], // Optional: filter by tags
90
+ limit: 10, // Optional: max results (default 20, max 100)
91
+ offset: 0 // Optional: pagination offset
92
+ })
93
+ ```
94
+
95
+ Returns entry metadata (id, title, kind, category, tags, created_at) without body content. Use `get_context` with a search query to retrieve full entries.
96
+
97
+ ### `delete_context` — Remove an entry
98
+
99
+ ```js
100
+ delete_context({
101
+ id: "01HXYZ..." // ULID of the entry to delete
102
+ })
103
+ ```
104
+
105
+ Removes the markdown file from disk and cleans up the database and vector index.
106
+
167
107
  ### `context_status` — Diagnostics
168
108
 
169
109
  Shows vault path, database size, file counts per kind, embedding coverage, and any issues.
@@ -175,19 +115,19 @@ Shows vault path, database size, file counts per kind, embedding coverage, and a
175
115
  Each top-level subdirectory in the vault maps to a `kind` value. The directory name is depluralized:
176
116
 
177
117
  ```
178
- insights/ → kind: "insight"
179
- decisions/ → kind: "decision"
180
- patterns/ → kind: "pattern"
181
- references/ → kind: "reference"
118
+ knowledge/insights/ → kind: "insight"
119
+ knowledge/decisions/ → kind: "decision"
120
+ knowledge/patterns/ → kind: "pattern"
121
+ knowledge/references/ → kind: "reference"
182
122
  ```
183
123
 
184
124
  Within each kind directory, nested subfolders provide human-browsable organization. The subfolder path is stored in `meta.folder`:
185
125
 
186
126
  ```
187
127
  ON DISK IN DB (vault table)
188
- insights/ kind: "insight", meta.folder: null
128
+ knowledge/insights/ kind: "insight", meta.folder: null
189
129
  flat-file.md
190
- insights/react/hooks/ kind: "insight", meta.folder: "react/hooks"
130
+ knowledge/insights/react/hooks/ kind: "insight", meta.folder: "react/hooks"
191
131
  use-query-gotcha.md
192
132
  ```
193
133
 
@@ -213,7 +153,7 @@ Standard keys: `id`, `tags`, `source`, `created`. Any extra frontmatter keys (`t
213
153
 
214
154
  No code changes required:
215
155
 
216
- 1. `mkdir ~/vault/references/`
156
+ 1. `mkdir ~/vault/knowledge/references/`
217
157
  2. Add `.md` files with YAML frontmatter
218
158
  3. The next session auto-indexes them
219
159
 
@@ -281,62 +221,125 @@ context-mcp <command> [options]
281
221
 
282
222
  If running from source without a global install, use `node bin/cli.js` instead of `context-mcp`.
283
223
 
284
- ## Desktop App (macOS)
224
+ ## Install
285
225
 
286
- A macOS dock application that starts the UI server and opens the dashboard in your browser.
226
+ ### npm (Recommended)
287
227
 
288
228
  ```bash
289
- osacompile -o "/Applications/Context.app" ui/Context.applescript
229
+ npm install -g context-vault
230
+ context-mcp setup
290
231
  ```
291
232
 
292
- The app checks if port 3141 is already in use, starts the server if not, and opens `http://localhost:3141`. Server logs are written to `/tmp/context-mcp.log`.
233
+ The `setup` command auto-detects installed tools (Claude Code, Claude Desktop, Cursor, Windsurf, Cline), lets you pick which to configure, and writes the correct MCP config for each. Existing configs are preserved only the `context-mcp` entry is added or updated.
293
234
 
294
- ## Troubleshooting
235
+ ### Local Development
295
236
 
296
- ### Native module build failures
237
+ ```bash
238
+ git clone https://github.com/fellanH/context-mcp.git
239
+ cd context-mcp
240
+ npm install
297
241
 
298
- `better-sqlite3` and `sqlite-vec` include native C code compiled for your platform. If install fails:
242
+ # Interactive setup detects your tools and configures them
243
+ node bin/cli.js setup
244
+ ```
245
+
246
+ For non-interactive environments (CI, scripts):
299
247
 
300
248
  ```bash
301
- npm rebuild better-sqlite3 sqlite-vec
249
+ context-mcp setup --yes
302
250
  ```
303
251
 
304
- On Apple Silicon Macs, ensure you're running a native ARM Node.js (not Rosetta). Check with `node -p process.arch` — it should say `arm64`.
252
+ ### Manual Configuration
305
253
 
306
- ### Vault directory not found
254
+ If you prefer manual setup, add to your tool's MCP config. Pass `--vault-dir` to point at your vault folder (omit it to use the default `~/vault/`).
307
255
 
308
- If `context_status` or `get_context` reports the vault directory doesn't exist:
256
+ **npm install** (portable survives upgrades):
309
257
 
310
- ```bash
311
- context-mcp status # Shows resolved paths
312
- mkdir -p ~/vault # Create the default vault directory
258
+ ```json
259
+ {
260
+ "mcpServers": {
261
+ "context-mcp": {
262
+ "command": "context-mcp",
263
+ "args": ["serve", "--vault-dir", "/path/to/vault"]
264
+ }
265
+ }
266
+ }
313
267
  ```
314
268
 
315
- Or re-run `context-mcp setup` to reconfigure.
269
+ **Local dev clone** (absolute path to source):
270
+
271
+ ```json
272
+ {
273
+ "mcpServers": {
274
+ "context-mcp": {
275
+ "command": "node",
276
+ "args": ["/path/to/context-mcp/src/server/index.js", "--vault-dir", "/path/to/vault"]
277
+ }
278
+ }
279
+ }
280
+ ```
316
281
 
317
- ### Embedding model download stalls
282
+ You can also pass config via environment variables in the MCP config block:
318
283
 
319
- The first run downloads the all-MiniLM-L6-v2 embedding model (~22MB) from Hugging Face. This requires internet access and can take a moment. If it hangs, check your network or proxy settings.
284
+ ```json
285
+ {
286
+ "mcpServers": {
287
+ "context-mcp": {
288
+ "command": "context-mcp",
289
+ "args": ["serve"],
290
+ "env": {
291
+ "CONTEXT_MCP_VAULT_DIR": "/path/to/vault"
292
+ }
293
+ }
294
+ }
295
+ }
296
+ ```
320
297
 
321
- ### Stale search index
298
+ ### How the Server Runs
322
299
 
323
- If search results seem outdated or missing:
300
+ The server is an MCP (Model Context Protocol) process — you don't start or stop it manually. Your AI client (Claude Code, Cursor, Cline, etc.) spawns it automatically as a child process when a session begins, based on the `mcpServers` config above. The server communicates over stdio and lives for the duration of the session. When the session ends, the client terminates the process and SQLite cleans up its WAL files.
301
+
302
+ This means:
303
+ - **No daemon, no port, no background service.** The server only runs while your AI client is active.
304
+ - **Multiple sessions** can run separate server instances concurrently — SQLite WAL mode handles concurrent access safely.
305
+ - **Embedding model** is downloaded during `setup` (~22MB, all-MiniLM-L6-v2). If setup was skipped, it downloads on first use.
306
+ - **Auto-reindex** on first tool call per session ensures the search index is always in sync with your files on disk. No manual reindex needed.
307
+
308
+ ## Desktop App (macOS)
309
+
310
+ A macOS dock application that starts the UI server and opens the dashboard in your browser.
324
311
 
325
312
  ```bash
326
- context-mcp reindex
313
+ osacompile -o "/Applications/Context.app" ui/Context.applescript
327
314
  ```
328
315
 
329
- This rebuilds the entire index from your vault files. Auto-reindex runs on every session start, but manual reindex can help diagnose issues.
316
+ The app checks if port 3141 is already in use, starts the server if not, and opens `http://localhost:3141`. Server logs are written to `/tmp/context-mcp.log`.
330
317
 
331
- ### Config path debugging
318
+ ## How It Works
332
319
 
333
- ```bash
334
- context-mcp status
320
+ ```
321
+ YOUR FILES (source of truth) SEARCH INDEX (derived)
322
+ ~/vault/ ~/.context-mcp/vault.db
323
+ ├── knowledge/ ┌───────────────────────────────┐
324
+ │ ├── insights/ │ vault table │
325
+ │ │ ├── react-query-caching.md │ kind: insight │
326
+ │ │ └── react/hooks/ │ meta.folder: "react/hooks" │
327
+ │ │ └── use-query-gotcha.md │ kind: decision │
328
+ │ ├── decisions/ │ kind: pattern │
329
+ │ │ └── use-sqlite-over-pg.md │ kind: <any custom> │
330
+ │ └── patterns/ │ + FTS5 full-text │
331
+ │ └── api-error-handler.md │ + vec0 embeddings │
332
+ ├── entities/ └───────────────────────────────┘
333
+ └── events/
334
+ Human-editable, git-versioned Fast hybrid search, RAG-ready
335
+ You own these files Rebuilt from files anytime
335
336
  ```
336
337
 
337
- Shows all resolved paths (vault dir, data dir, DB path, config file) and where each was resolved from (defaults, config file, env, or CLI args).
338
+ The SQLite database is stored at `~/.context-mcp/vault.db` by default (configurable via `--db-path`, `CONTEXT_MCP_DB_PATH`, or `config.json`). It contains FTS5 full-text indexes and sqlite-vec embeddings (384-dim float32, all-MiniLM-L6-v2). The database is a derived index — delete it and the server rebuilds it automatically on next session.
339
+
340
+ Requires **Node.js 20** or later.
338
341
 
339
- ## Architecture
342
+ ### Architecture
340
343
 
341
344
  ```
342
345
  src/
@@ -369,6 +372,51 @@ index/embed.js ← retrieve/ ← ui/serve.js
369
372
  index/db.js ←────────────────── (all consumers)
370
373
  ```
371
374
 
375
+ ## Troubleshooting
376
+
377
+ ### Native module build failures
378
+
379
+ `better-sqlite3` and `sqlite-vec` include native C code compiled for your platform. If install fails:
380
+
381
+ ```bash
382
+ npm rebuild better-sqlite3 sqlite-vec
383
+ ```
384
+
385
+ On Apple Silicon Macs, ensure you're running a native ARM Node.js (not Rosetta). Check with `node -p process.arch` — it should say `arm64`.
386
+
387
+ ### Vault directory not found
388
+
389
+ If `context_status` or `get_context` reports the vault directory doesn't exist:
390
+
391
+ ```bash
392
+ context-mcp status # Shows resolved paths
393
+ mkdir -p ~/vault # Create the default vault directory
394
+ ```
395
+
396
+ Or re-run `context-mcp setup` to reconfigure.
397
+
398
+ ### Embedding model download
399
+
400
+ The embedding model (all-MiniLM-L6-v2, ~22MB) is normally downloaded during `context-mcp setup`. If setup was skipped or the cache was cleared, it downloads automatically on first use. If it hangs, check your network or proxy settings.
401
+
402
+ ### Stale search index
403
+
404
+ If search results seem outdated or missing:
405
+
406
+ ```bash
407
+ context-mcp reindex
408
+ ```
409
+
410
+ This rebuilds the entire index from your vault files. Auto-reindex runs on every session start, but manual reindex can help diagnose issues.
411
+
412
+ ### Config path debugging
413
+
414
+ ```bash
415
+ context-mcp status
416
+ ```
417
+
418
+ Shows all resolved paths (vault dir, data dir, DB path, config file) and where each was resolved from (defaults, config file, env, or CLI args).
419
+
372
420
  ## Dependencies
373
421
 
374
422
  | Package | Purpose |
package/bin/cli.js CHANGED
@@ -156,7 +156,8 @@ const TOOLS = [
156
156
 
157
157
  function showHelp() {
158
158
  console.log(`
159
- ${bold("context-mcp")} v${VERSION} — Personal knowledge vault for AI agents
159
+ ${bold("context-vault")} ${dim(`v${VERSION}`)}
160
+ ${dim("Persistent memory for AI agents")}
160
161
 
161
162
  ${bold("Usage:")}
162
163
  context-mcp <command> [options]
@@ -180,12 +181,12 @@ ${bold("Options:")}
180
181
  async function runSetup() {
181
182
  // Banner
182
183
  console.log();
183
- console.log(bold(" context-mcp") + dim(` v${VERSION}`));
184
- console.log(dim(" Personal knowledge vault for AI agents"));
184
+ console.log(` ${bold("context-vault")} ${dim(`v${VERSION}`)}`);
185
+ console.log(dim(" Persistent memory for AI agents"));
185
186
  console.log();
186
187
 
187
188
  // Detect tools
188
- console.log(bold(" Detecting installed tools...\n"));
189
+ console.log(dim(` [1/5]`) + bold(" Detecting tools...\n"));
189
190
  const detected = [];
190
191
  for (const tool of TOOLS) {
191
192
  const found = tool.detect();
@@ -230,11 +231,13 @@ async function runSetup() {
230
231
  if (isNonInteractive) {
231
232
  selected = detected;
232
233
  } else {
233
- const listing = detected
234
- .map((t, i) => `${i + 1}) ${t.name}`)
235
- .join(" ");
234
+ console.log(bold(" Which tools should context-mcp connect to?\n"));
235
+ for (let i = 0; i < detected.length; i++) {
236
+ console.log(` ${i + 1}) ${detected[i].name}`);
237
+ }
238
+ console.log();
236
239
  const answer = await prompt(
237
- ` Configure: ${dim("all")} or enter numbers (${listing}):`,
240
+ ` Select (${dim("1,2,3")} or ${dim('"all"')}):`,
238
241
  "all"
239
242
  );
240
243
  if (answer === "all" || answer === "") {
@@ -250,6 +253,7 @@ async function runSetup() {
250
253
  }
251
254
 
252
255
  // Vault directory (content files)
256
+ console.log(dim(` [2/5]`) + bold(" Configuring vault...\n"));
253
257
  const defaultVaultDir = join(HOME, "vault");
254
258
  const vaultDir = isNonInteractive
255
259
  ? defaultVaultDir
@@ -294,6 +298,17 @@ async function runSetup() {
294
298
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
295
299
  console.log(`\n ${green("+")} Wrote ${configPath}`);
296
300
 
301
+ // Pre-download embedding model
302
+ console.log(`\n ${dim("[3/5]")}${bold(" Downloading embedding model...")}`);
303
+ console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
304
+ try {
305
+ const { embed } = await import("../src/index/embed.js");
306
+ await embed("warmup");
307
+ console.log(` ${green("+")} Embedding model ready`);
308
+ } catch (e) {
309
+ console.log(` ${yellow("!")} Model download failed — will retry on first use`);
310
+ }
311
+
297
312
  // Clean up legacy project-root config.json if it exists
298
313
  const legacyConfigPath = join(ROOT, "config.json");
299
314
  if (existsSync(legacyConfigPath)) {
@@ -304,7 +319,7 @@ async function runSetup() {
304
319
  }
305
320
 
306
321
  // Configure each tool — pass vault dir as arg if non-default
307
- console.log(`\n${bold(" Configuring tools...\n")}`);
322
+ console.log(`\n ${dim("[4/5]")}${bold(" Configuring tools...\n")}`);
308
323
  const results = [];
309
324
  const defaultVDir = join(HOME, "vault");
310
325
  const customVaultDir = resolvedVaultDir !== resolve(defaultVDir) ? resolvedVaultDir : null;
@@ -324,6 +339,12 @@ async function runSetup() {
324
339
  }
325
340
  }
326
341
 
342
+ // Seed entry
343
+ const seeded = createSeedEntry(resolvedVaultDir);
344
+ if (seeded) {
345
+ console.log(`\n ${green("+")} Created starter entry in vault`);
346
+ }
347
+
327
348
  // Offer to launch UI
328
349
  console.log();
329
350
  if (!isNonInteractive) {
@@ -338,21 +359,35 @@ async function runSetup() {
338
359
  }
339
360
  }
340
361
 
341
- // Summary
342
- console.log(bold("\n Setup complete!\n"));
343
- const ok = results.filter((r) => r.ok);
344
- if (ok.length) {
345
- console.log(
346
- ` ${green(ok.length)} tool${ok.length > 1 ? "s" : ""} configured:`
347
- );
348
- for (const r of ok) {
349
- console.log(` ${r.tool.name}`);
350
- }
362
+ // Health check
363
+ console.log(`\n ${dim("[5/5]")}${bold(" Health check...")}\n`);
364
+ const okResults = results.filter((r) => r.ok);
365
+ const checks = [
366
+ { label: "Vault directory exists", pass: existsSync(resolvedVaultDir) },
367
+ { label: "Config file written", pass: existsSync(configPath) },
368
+ { label: "At least one tool configured", pass: okResults.length > 0 },
369
+ ];
370
+ const passed = checks.filter((c) => c.pass).length;
371
+ for (const c of checks) {
372
+ console.log(` ${c.pass ? green("✓") : red("✗")} ${c.label}`);
351
373
  }
352
- console.log(`\n Vault dir: ${resolvedVaultDir}`);
353
- console.log(` Config: ${configPath}`);
354
- console.log(` MCP server: ${isInstalledPackage() ? "context-mcp serve" : SERVER_PATH}`);
355
- console.log(`\n Run ${cyan("context-mcp ui")} to open the dashboard.`);
374
+
375
+ // Completion box
376
+ const toolName = okResults.length ? okResults[0].tool.name : "your AI tool";
377
+ const boxLines = [
378
+ ` ✓ Setup complete — ${passed}/${checks.length} checks passed`,
379
+ ``,
380
+ ` Open ${toolName} and try:`,
381
+ ` "Search my vault for getting started"`,
382
+ ];
383
+ const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
384
+ const pad = (s) => s + " ".repeat(Math.max(0, innerWidth - s.length));
385
+ console.log();
386
+ console.log(` ${dim("┌" + "─".repeat(innerWidth) + "┐")}`);
387
+ for (const line of boxLines) {
388
+ console.log(` ${dim("│")}${pad(line)}${dim("│")}`);
389
+ }
390
+ console.log(` ${dim("└" + "─".repeat(innerWidth) + "┘")}`);
356
391
  console.log();
357
392
  }
358
393
 
@@ -431,6 +466,38 @@ function configureJsonTool(tool, vaultDir) {
431
466
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
432
467
  }
433
468
 
469
+ // ─── Seed Entry ─────────────────────────────────────────────────────────────
470
+
471
+ function createSeedEntry(vaultDir) {
472
+ const seedDir = join(vaultDir, "knowledge", "insights");
473
+ const seedPath = join(seedDir, "getting-started.md");
474
+ if (existsSync(seedPath)) return false;
475
+ mkdirSync(seedDir, { recursive: true });
476
+ const id = Date.now().toString(36).toUpperCase().padStart(10, "0");
477
+ const now = new Date().toISOString();
478
+ const content = `---
479
+ id: ${id}
480
+ tags: ["getting-started"]
481
+ source: context-mcp-setup
482
+ created: ${now}
483
+ ---
484
+ Welcome to your context vault! This is a seed entry created during setup.
485
+
486
+ Your vault stores knowledge as plain markdown files with YAML frontmatter.
487
+ AI agents search it using hybrid full-text + semantic search.
488
+
489
+ Try these commands in your AI tool:
490
+ - "Search my vault for getting started"
491
+ - "Save an insight: JavaScript Date objects are mutable"
492
+ - "Show my vault status"
493
+
494
+ You can edit or delete this file anytime — it lives at:
495
+ ${seedPath}
496
+ `;
497
+ writeFileSync(seedPath, content);
498
+ return true;
499
+ }
500
+
434
501
  // ─── UI Command ──────────────────────────────────────────────────────────────
435
502
 
436
503
  function runUi() {
@@ -478,11 +545,11 @@ async function runReindex() {
478
545
  const stats = await reindex(ctx, { fullSync: true });
479
546
 
480
547
  db.close();
481
- console.log(green("Reindex complete:"));
482
- console.log(` Added: ${stats.added}`);
483
- console.log(` Updated: ${stats.updated}`);
484
- console.log(` Removed: ${stats.removed}`);
485
- console.log(` Unchanged: ${stats.unchanged}`);
548
+ console.log(green("Reindex complete"));
549
+ console.log(` ${green("+")} ${stats.added} added`);
550
+ console.log(` ${yellow("~")} ${stats.updated} updated`);
551
+ console.log(` ${red("-")} ${stats.removed} removed`);
552
+ console.log(` ${dim("·")} ${stats.unchanged} unchanged`);
486
553
  }
487
554
 
488
555
  // ─── Status Command ──────────────────────────────────────────────────────────
@@ -500,26 +567,43 @@ async function runStatus() {
500
567
  db.close();
501
568
 
502
569
  console.log();
503
- console.log(bold(" Vault Status"));
570
+ console.log(` ${bold(" context-vault")} ${dim(`v${VERSION}`)}`);
504
571
  console.log();
505
- console.log(` Vault: ${config.vaultDir} (exists: ${config.vaultDirExists}, ${status.fileCount} files)`);
506
- console.log(` Database: ${config.dbPath} (${status.dbSize})`);
572
+ console.log(` Vault: ${config.vaultDir} ${dim(`(${config.vaultDirExists ? status.fileCount + " files" : "missing"})`)}`);
573
+ console.log(` Database: ${config.dbPath} ${dim(`(${status.dbSize})`)}`);
507
574
  console.log(` Dev dir: ${config.devDir}`);
508
575
  console.log(` Data dir: ${config.dataDir}`);
509
- console.log(` Config: ${config.configPath} (exists: ${existsSync(config.configPath)})`);
576
+ console.log(` Config: ${config.configPath} ${dim(`(${existsSync(config.configPath) ? "exists" : "missing"})`)}`);
510
577
  console.log(` Resolved: ${status.resolvedFrom}`);
511
578
  console.log(` Schema: v5 (categories)`);
512
579
 
513
580
  if (status.kindCounts.length) {
581
+ const BAR_WIDTH = 20;
582
+ const maxCount = Math.max(...status.kindCounts.map((k) => k.c));
514
583
  console.log();
515
584
  console.log(bold(" Indexed"));
516
585
  for (const { kind, c } of status.kindCounts) {
517
- console.log(` ${c} ${kind}s`);
586
+ const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
587
+ const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
588
+ const countStr = String(c).padStart(4);
589
+ console.log(` ${countStr} ${kind}s ${dim(bar)}`);
518
590
  }
519
591
  } else {
520
592
  console.log(`\n ${dim("(empty — no entries indexed)")}`);
521
593
  }
522
594
 
595
+ if (status.embeddingStatus) {
596
+ const { indexed, total, missing } = status.embeddingStatus;
597
+ if (missing > 0) {
598
+ const BAR_WIDTH = 20;
599
+ const filled = total > 0 ? Math.round((indexed / total) * BAR_WIDTH) : 0;
600
+ const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
601
+ const pct = total > 0 ? Math.round((indexed / total) * 100) : 0;
602
+ console.log();
603
+ console.log(` Embeddings ${dim(bar)} ${indexed}/${total} (${pct}%)`);
604
+ }
605
+ }
606
+
523
607
  if (status.subdirs.length) {
524
608
  console.log();
525
609
  console.log(bold(" Disk Directories"));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
- "description": "Personal context vault connects any AI agent to your accumulated knowledge",
5
+ "description": "Persistent memory for AI agents saves and searches knowledge across sessions",
6
6
  "bin": {
7
7
  "context-mcp": "bin/cli.js"
8
8
  },
@@ -21,6 +21,13 @@
21
21
  "repository": { "type": "git", "url": "https://github.com/fellanH/context-mcp.git" },
22
22
  "homepage": "https://github.com/fellanH/context-mcp",
23
23
  "keywords": ["mcp", "model-context-protocol", "ai", "knowledge-base", "knowledge-management", "vault", "rag", "sqlite", "embeddings", "claude", "cursor", "cline", "windsurf"],
24
+ "scripts": {
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
28
+ "devDependencies": {
29
+ "vitest": "^3.0.0"
30
+ },
24
31
  "dependencies": {
25
32
  "@huggingface/transformers": "^3.0.0",
26
33
  "@modelcontextprotocol/sdk": "^1.26.0",
package/smithery.yaml CHANGED
@@ -7,4 +7,4 @@ startCommand:
7
7
  type: string
8
8
  description: "Path to the vault directory containing your knowledge files. Defaults to ~/vault/"
9
9
  commandFunction: |-
10
- (config) => ({ command: 'npx', args: ['-y', '@fellanh/context-mcp', 'serve', ...(config.vaultDir ? ['--vault-dir', config.vaultDir] : [])] })
10
+ (config) => ({ command: 'npx', args: ['-y', 'context-vault', 'serve', ...(config.vaultDir ? ['--vault-dir', config.vaultDir] : [])] })
@@ -11,7 +11,8 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
  import { ulid, slugify, kindToPath } from "../core/files.js";
13
13
  import { categoryFor } from "../core/categories.js";
14
- import { parseFrontmatter } from "../core/frontmatter.js";
14
+ import { parseFrontmatter, formatFrontmatter } from "../core/frontmatter.js";
15
+ import { formatBody } from "./formatters.js";
15
16
  import { writeEntryFile } from "./file-ops.js";
16
17
 
17
18
  export function writeEntry(ctx, { kind, title, body, meta, tags, source, folder, identity_key, expires_at }) {
@@ -61,6 +62,70 @@ export function writeEntry(ctx, { kind, title, body, meta, tags, source, folder,
61
62
  return { id, filePath, kind, category, title, body, meta, tags, source, createdAt, identity_key, expires_at };
62
63
  }
63
64
 
65
+ /**
66
+ * Update an existing entry's file on disk (merge provided fields with existing).
67
+ * Does NOT re-index — caller must call indexEntry after.
68
+ *
69
+ * @param {{ config, stmts }} ctx
70
+ * @param {object} existing — Row from vault table (from getEntryById)
71
+ * @param {{ title?, body?, tags?, meta?, source?, expires_at? }} updates
72
+ * @returns {object} Entry object suitable for indexEntry
73
+ */
74
+ export function updateEntryFile(ctx, existing, updates) {
75
+ const raw = readFileSync(existing.file_path, "utf-8");
76
+ const { meta: fmMeta } = parseFrontmatter(raw);
77
+
78
+ const existingMeta = existing.meta ? JSON.parse(existing.meta) : {};
79
+ const existingTags = existing.tags ? JSON.parse(existing.tags) : [];
80
+
81
+ const title = updates.title !== undefined ? updates.title : existing.title;
82
+ const body = updates.body !== undefined ? updates.body : existing.body;
83
+ const tags = updates.tags !== undefined ? updates.tags : existingTags;
84
+ const source = updates.source !== undefined ? updates.source : existing.source;
85
+ const expires_at = updates.expires_at !== undefined ? updates.expires_at : existing.expires_at;
86
+
87
+ let mergedMeta;
88
+ if (updates.meta !== undefined) {
89
+ mergedMeta = { ...existingMeta, ...(updates.meta || {}) };
90
+ } else {
91
+ mergedMeta = { ...existingMeta };
92
+ }
93
+
94
+ // Build frontmatter
95
+ const fmFields = { id: existing.id };
96
+ for (const [k, v] of Object.entries(mergedMeta)) {
97
+ if (k === "folder") continue;
98
+ if (v !== null && v !== undefined) fmFields[k] = v;
99
+ }
100
+ if (existing.identity_key) fmFields.identity_key = existing.identity_key;
101
+ if (expires_at) fmFields.expires_at = expires_at;
102
+ fmFields.tags = tags;
103
+ fmFields.source = source || "claude-code";
104
+ fmFields.created = fmMeta.created || existing.created_at;
105
+
106
+ const mdBody = formatBody(existing.kind, { title, body, meta: mergedMeta });
107
+ const md = formatFrontmatter(fmFields) + mdBody;
108
+
109
+ writeFileSync(existing.file_path, md);
110
+
111
+ const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
112
+
113
+ return {
114
+ id: existing.id,
115
+ filePath: existing.file_path,
116
+ kind: existing.kind,
117
+ category: existing.category,
118
+ title,
119
+ body,
120
+ meta: finalMeta,
121
+ tags,
122
+ source,
123
+ createdAt: fmMeta.created || existing.created_at,
124
+ identity_key: existing.identity_key,
125
+ expires_at,
126
+ };
127
+ }
128
+
64
129
  export async function captureAndIndex(ctx, data, indexFn) {
65
130
  // For entity upserts, preserve previous file content for safe rollback
66
131
  let previousContent = null;
package/src/index/db.js CHANGED
@@ -114,6 +114,7 @@ export function prepareStatements(db) {
114
114
  deleteEntry: db.prepare(`DELETE FROM vault WHERE id = ?`),
115
115
  getRowid: db.prepare(`SELECT rowid FROM vault WHERE id = ?`),
116
116
  getRowidByPath: db.prepare(`SELECT rowid FROM vault WHERE file_path = ?`),
117
+ getEntryById: db.prepare(`SELECT * FROM vault WHERE id = ?`),
117
118
  getByIdentityKey: db.prepare(`SELECT * FROM vault WHERE kind = ? AND identity_key = ?`),
118
119
  upsertByIdentityKey: db.prepare(`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, file_path = ?, expires_at = ? WHERE kind = ? AND identity_key = ?`),
119
120
  insertVecStmt: db.prepare(`INSERT INTO vault_vec (rowid, embedding) VALUES (?, ?)`),
@@ -9,10 +9,11 @@ let extractor = null;
9
9
  async function ensurePipeline() {
10
10
  if (!extractor) {
11
11
  try {
12
+ console.error("[context-mcp] Loading embedding model (first run may download ~22MB)...");
12
13
  extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
13
14
  } catch (e) {
14
15
  console.error(`[context-mcp] Failed to load embedding model: ${e.message}`);
15
- console.error(`[context-mcp] The model (~80 MB) is downloaded on first run.`);
16
+ console.error(`[context-mcp] The model (~22MB) is downloaded on first run.`);
16
17
  console.error(`[context-mcp] Check: network connectivity, disk space, Node.js >=20`);
17
18
  throw e;
18
19
  }
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * tools.js — MCP tool registrations
3
3
  *
4
- * Three tools: save_context (write), get_context (read), context_status (diag).
4
+ * Five tools: save_context (write/update), get_context (search), list_context (browse),
5
+ * delete_context (remove), context_status (diag).
5
6
  * Auto-reindex runs transparently on first tool call per session.
6
7
  */
7
8
 
8
9
  import { z } from "zod";
9
- import { existsSync } from "node:fs";
10
+ import { existsSync, unlinkSync } from "node:fs";
10
11
 
11
- import { captureAndIndex } from "../capture/index.js";
12
+ import { captureAndIndex, updateEntryFile } from "../capture/index.js";
12
13
  import { hybridSearch } from "../retrieve/index.js";
13
14
  import { reindex, indexEntry } from "../index/index.js";
14
15
  import { gatherVaultStatus } from "../core/status.js";
@@ -58,7 +59,7 @@ export function registerTools(server, ctx) {
58
59
  return reindexPromise;
59
60
  }
60
61
 
61
- // ─── get_context (read) ────────────────────────────────────────────────────
62
+ // ─── get_context (search) ──────────────────────────────────────────────────
62
63
 
63
64
  server.tool(
64
65
  "get_context",
@@ -98,10 +99,13 @@ export function registerTools(server, ctx) {
98
99
  const lines = [];
99
100
  if (reindexFailed) lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-mcp reindex\` to fix.\n`);
100
101
  lines.push(`## Results for "${query}" (${filtered.length} matches)\n`);
101
- for (const r of filtered) {
102
- const meta = r.meta ? JSON.parse(r.meta) : {};
103
- lines.push(`### ${r.title || "(untitled)"} [${r.kind}/${r.category}]`);
104
- lines.push(`Score: ${r.score.toFixed(3)} | Tags: ${r.tags || "none"} | File: ${r.file_path || "n/a"}`);
102
+ for (let i = 0; i < filtered.length; i++) {
103
+ const r = filtered[i];
104
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
105
+ const tagStr = entryTags.length ? entryTags.join(", ") : "none";
106
+ const relPath = r.file_path && config.vaultDir ? r.file_path.replace(config.vaultDir + "/", "") : r.file_path || "n/a";
107
+ lines.push(`### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]`);
108
+ lines.push(`${r.score.toFixed(3)} · ${tagStr} · ${relPath}`);
105
109
  lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
106
110
  lines.push("");
107
111
  }
@@ -109,15 +113,16 @@ export function registerTools(server, ctx) {
109
113
  }
110
114
  );
111
115
 
112
- // ─── save_context (write) ──────────────────────────────────────────────────
116
+ // ─── save_context (write / update) ────────────────────────────────────────
113
117
 
114
118
  server.tool(
115
119
  "save_context",
116
120
  "Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind.",
117
121
  {
118
- kind: z.string().describe("Entry kind determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind)"),
122
+ id: z.string().optional().describe("Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved."),
123
+ kind: z.string().optional().describe("Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."),
119
124
  title: z.string().optional().describe("Entry title (optional for insights)"),
120
- body: z.string().describe("Main content"),
125
+ body: z.string().optional().describe("Main content. Required for new entries."),
121
126
  tags: z.array(z.string()).optional().describe("Tags for categorization and search"),
122
127
  meta: z.any().optional().describe("Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"),
123
128
  folder: z.string().optional().describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
@@ -125,14 +130,40 @@ export function registerTools(server, ctx) {
125
130
  identity_key: z.string().optional().describe("Required for entity kinds (contact, project, tool, source). The unique identifier for this entity."),
126
131
  expires_at: z.string().optional().describe("ISO date for TTL expiry"),
127
132
  },
128
- async ({ kind, title, body, tags, meta, folder, source, identity_key, expires_at }) => {
133
+ async ({ id, kind, title, body, tags, meta, folder, source, identity_key, expires_at }) => {
129
134
  const vaultErr = ensureVaultExists(config);
130
135
  if (vaultErr) return vaultErr;
136
+
137
+ // ── Update mode ──
138
+ if (id) {
139
+ await ensureIndexed();
140
+
141
+ const existing = ctx.stmts.getEntryById.get(id);
142
+ if (!existing) return err(`Entry not found: ${id}`, "NOT_FOUND");
143
+
144
+ if (kind && normalizeKind(kind) !== existing.kind) {
145
+ return err(`Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`, "INVALID_UPDATE");
146
+ }
147
+ if (identity_key && identity_key !== existing.identity_key) {
148
+ return err(`Cannot change identity_key (current: "${existing.identity_key}"). Delete and re-create instead.`, "INVALID_UPDATE");
149
+ }
150
+
151
+ const entry = updateEntryFile(ctx, existing, { title, body, tags, meta, source, expires_at });
152
+ await indexEntry(ctx, entry);
153
+ const relPath = entry.filePath ? entry.filePath.replace(config.vaultDir + "/", "") : entry.filePath;
154
+ const parts = [`✓ Updated ${entry.kind} → ${relPath}`, ` id: ${entry.id}`];
155
+ if (entry.title) parts.push(` title: ${entry.title}`);
156
+ const entryTags = entry.tags || [];
157
+ if (entryTags.length) parts.push(` tags: ${entryTags.join(", ")}`);
158
+ return ok(parts.join("\n"));
159
+ }
160
+
161
+ // ── Create mode ──
162
+ if (!kind) return err("Required: kind (for new entries)", "INVALID_INPUT");
131
163
  const kindErr = ensureValidKind(kind);
132
164
  if (kindErr) return kindErr;
133
- if (!body?.trim()) return err("Required: body (non-empty string)", "INVALID_INPUT");
165
+ if (!body?.trim()) return err("Required: body (for new entries)", "INVALID_INPUT");
134
166
 
135
- // Validate: entity kinds require identity_key
136
167
  if (categoryFor(kind) === "entity" && !identity_key) {
137
168
  return err(`Entity kind "${kind}" requires identity_key`, "MISSING_IDENTITY_KEY");
138
169
  }
@@ -144,7 +175,117 @@ export function registerTools(server, ctx) {
144
175
  const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
145
176
 
146
177
  const entry = await captureAndIndex(ctx, { kind, title, body, meta: finalMeta, tags, source, folder, identity_key, expires_at }, indexEntry);
147
- return ok(`Saved ${kind} ${entry.id}\nFile: ${entry.filePath}${title ? "\nTitle: " + title : ""}`);
178
+ const relPath = entry.filePath ? entry.filePath.replace(config.vaultDir + "/", "") : entry.filePath;
179
+ const parts = [`✓ Saved ${kind} → ${relPath}`, ` id: ${entry.id}`];
180
+ if (title) parts.push(` title: ${title}`);
181
+ if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
182
+ return ok(parts.join("\n"));
183
+ }
184
+ );
185
+
186
+ // ─── list_context (browse) ────────────────────────────────────────────────
187
+
188
+ server.tool(
189
+ "list_context",
190
+ "Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at. Use get_context with a query for semantic search.",
191
+ {
192
+ kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
193
+ category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
194
+ tags: z.array(z.string()).optional().describe("Filter by tags (entries must match at least one)"),
195
+ since: z.string().optional().describe("ISO date, return entries created after this"),
196
+ until: z.string().optional().describe("ISO date, return entries created before this"),
197
+ limit: z.number().optional().describe("Max results to return (default 20, max 100)"),
198
+ offset: z.number().optional().describe("Skip first N results for pagination"),
199
+ },
200
+ async ({ kind, category, tags, since, until, limit, offset }) => {
201
+ await ensureIndexed();
202
+
203
+ const clauses = [];
204
+ const params = [];
205
+
206
+ if (kind) {
207
+ clauses.push("kind = ?");
208
+ params.push(normalizeKind(kind));
209
+ }
210
+ if (category) {
211
+ clauses.push("category = ?");
212
+ params.push(category);
213
+ }
214
+ if (since) {
215
+ clauses.push("created_at >= ?");
216
+ params.push(since);
217
+ }
218
+ if (until) {
219
+ clauses.push("created_at <= ?");
220
+ params.push(until);
221
+ }
222
+ clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
223
+
224
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
225
+ const effectiveLimit = Math.min(limit || 20, 100);
226
+ const effectiveOffset = offset || 0;
227
+
228
+ const countParams = [...params];
229
+ const total = ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...countParams).c;
230
+
231
+ params.push(effectiveLimit, effectiveOffset);
232
+ const rows = ctx.db.prepare(`SELECT id, title, kind, category, tags, created_at FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params);
233
+
234
+ // Post-filter by tags if provided
235
+ const filtered = tags?.length
236
+ ? rows.filter((r) => {
237
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
238
+ return tags.some((t) => entryTags.includes(t));
239
+ })
240
+ : rows;
241
+
242
+ if (!filtered.length) return ok("No entries found matching the given filters.");
243
+
244
+ const lines = [`## Vault Entries (${filtered.length} shown, ${total} total)\n`];
245
+ for (const r of filtered) {
246
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
247
+ const tagStr = entryTags.length ? entryTags.join(", ") : "none";
248
+ lines.push(`- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] — ${tagStr} — ${r.created_at} — \`${r.id}\``);
249
+ }
250
+
251
+ if (effectiveOffset + effectiveLimit < total) {
252
+ lines.push(`\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`);
253
+ }
254
+
255
+ return ok(lines.join("\n"));
256
+ }
257
+ );
258
+
259
+ // ─── delete_context (remove) ──────────────────────────────────────────────
260
+
261
+ server.tool(
262
+ "delete_context",
263
+ "Delete an entry from your vault by its ULID id. Removes the file from disk and cleans up the search index.",
264
+ {
265
+ id: z.string().describe("The entry ULID to delete"),
266
+ },
267
+ async ({ id }) => {
268
+ if (!id?.trim()) return err("Required: id (non-empty string)", "INVALID_INPUT");
269
+ await ensureIndexed();
270
+
271
+ const entry = ctx.stmts.getEntryById.get(id);
272
+ if (!entry) return err(`Entry not found: ${id}`, "NOT_FOUND");
273
+
274
+ // Delete file from disk first (source of truth)
275
+ if (entry.file_path) {
276
+ try { unlinkSync(entry.file_path); } catch {}
277
+ }
278
+
279
+ // Delete vector embedding
280
+ const rowidResult = ctx.stmts.getRowid.get(id);
281
+ if (rowidResult?.rowid) {
282
+ try { ctx.deleteVec(Number(rowidResult.rowid)); } catch {}
283
+ }
284
+
285
+ // Delete DB row (FTS trigger handles FTS cleanup)
286
+ ctx.stmts.deleteEntry.run(id);
287
+
288
+ return ok(`Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`);
148
289
  }
149
290
  );
150
291
 
@@ -157,20 +298,29 @@ export function registerTools(server, ctx) {
157
298
  () => {
158
299
  const status = gatherVaultStatus(ctx);
159
300
 
301
+ const hasIssues = status.stalePaths || (status.embeddingStatus?.missing > 0);
302
+ const healthIcon = hasIssues ? "⚠" : "✓";
303
+
160
304
  const lines = [
161
- `## Vault Status`,
305
+ `## ${healthIcon} Vault Status`,
162
306
  ``,
163
- `Vault: ${config.vaultDir} (exists: ${config.vaultDirExists}, ${status.fileCount} files)`,
164
- `Database: ${config.dbPath} (exists: ${existsSync(config.dbPath)}, ${status.dbSize})`,
307
+ `Vault: ${config.vaultDir} (${config.vaultDirExists ? status.fileCount + " files" : "missing"})`,
308
+ `Database: ${config.dbPath} (${status.dbSize})`,
165
309
  `Dev dir: ${config.devDir}`,
166
310
  `Data dir: ${config.dataDir}`,
167
311
  `Config: ${config.configPath}`,
168
312
  `Resolved via: ${status.resolvedFrom}`,
169
313
  `Schema: v5 (categories)`,
170
- ``,
171
- `### Indexed`,
172
314
  ];
173
315
 
316
+ if (status.embeddingStatus) {
317
+ const { indexed, total, missing } = status.embeddingStatus;
318
+ const pct = total > 0 ? Math.round((indexed / total) * 100) : 100;
319
+ lines.push(`Embeddings: ${indexed}/${total} (${pct}%)`);
320
+ }
321
+
322
+ lines.push(``, `### Indexed`);
323
+
174
324
  if (status.kindCounts.length) {
175
325
  for (const { kind, c } of status.kindCounts) lines.push(`- ${c} ${kind}s`);
176
326
  } else {
@@ -191,20 +341,11 @@ export function registerTools(server, ctx) {
191
341
 
192
342
  if (status.stalePaths) {
193
343
  lines.push(``);
194
- lines.push(`### Stale Paths Detected`);
344
+ lines.push(`### Stale Paths`);
195
345
  lines.push(`DB contains ${status.staleCount} paths not matching current vault dir.`);
196
346
  lines.push(`Auto-reindex will fix this on next search or save.`);
197
347
  }
198
348
 
199
- if (status.embeddingStatus) {
200
- const { indexed, total, missing } = status.embeddingStatus;
201
- if (missing > 0) {
202
- lines.push(``);
203
- lines.push(`### Embeddings`);
204
- lines.push(`${indexed}/${total} entries have embeddings (${missing} missing)`);
205
- }
206
- }
207
-
208
349
  return ok(lines.join("\n"));
209
350
  }
210
351
  );