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 +197 -149
- package/bin/cli.js +117 -33
- package/package.json +9 -2
- package/smithery.yaml +1 -1
- package/src/capture/index.js +66 -1
- package/src/index/db.js +1 -0
- package/src/index/embed.js +2 -1
- package/src/server/tools.js +171 -30
package/README.md
CHANGED
|
@@ -1,137 +1,43 @@
|
|
|
1
1
|
# context-mcp
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/context-vault)
|
|
4
|
+
[](https://www.npmjs.com/package/context-vault)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Persistent memory for AI agents — saves and searches knowledge across sessions.
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
## What It Does
|
|
118
26
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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/
|
|
128
|
+
knowledge/insights/ kind: "insight", meta.folder: null
|
|
189
129
|
flat-file.md
|
|
190
|
-
insights/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
|
-
##
|
|
224
|
+
## Install
|
|
285
225
|
|
|
286
|
-
|
|
226
|
+
### npm (Recommended)
|
|
287
227
|
|
|
288
228
|
```bash
|
|
289
|
-
|
|
229
|
+
npm install -g context-vault
|
|
230
|
+
context-mcp setup
|
|
290
231
|
```
|
|
291
232
|
|
|
292
|
-
The
|
|
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
|
-
|
|
235
|
+
### Local Development
|
|
295
236
|
|
|
296
|
-
|
|
237
|
+
```bash
|
|
238
|
+
git clone https://github.com/fellanH/context-mcp.git
|
|
239
|
+
cd context-mcp
|
|
240
|
+
npm install
|
|
297
241
|
|
|
298
|
-
|
|
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
|
-
|
|
249
|
+
context-mcp setup --yes
|
|
302
250
|
```
|
|
303
251
|
|
|
304
|
-
|
|
252
|
+
### Manual Configuration
|
|
305
253
|
|
|
306
|
-
|
|
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
|
-
|
|
256
|
+
**npm install** (portable — survives upgrades):
|
|
309
257
|
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
You can also pass config via environment variables in the MCP config block:
|
|
318
283
|
|
|
319
|
-
|
|
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
|
-
###
|
|
298
|
+
### How the Server Runs
|
|
322
299
|
|
|
323
|
-
|
|
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
|
-
|
|
313
|
+
osacompile -o "/Applications/Context.app" ui/Context.applescript
|
|
327
314
|
```
|
|
328
315
|
|
|
329
|
-
|
|
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
|
-
|
|
318
|
+
## How It Works
|
|
332
319
|
|
|
333
|
-
```
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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("
|
|
184
|
-
console.log(dim("
|
|
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("
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
.
|
|
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
|
-
`
|
|
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("
|
|
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
|
-
//
|
|
342
|
-
console.log(bold("
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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(`
|
|
483
|
-
console.log(`
|
|
484
|
-
console.log(`
|
|
485
|
-
console.log(`
|
|
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("
|
|
570
|
+
console.log(` ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}`);
|
|
504
571
|
console.log();
|
|
505
|
-
console.log(` Vault: ${config.vaultDir} (
|
|
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} (
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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', '
|
|
10
|
+
(config) => ({ command: 'npx', args: ['-y', 'context-vault', 'serve', ...(config.vaultDir ? ['--vault-dir', config.vaultDir] : [])] })
|
package/src/capture/index.js
CHANGED
|
@@ -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 (?, ?)`),
|
package/src/index/embed.js
CHANGED
|
@@ -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 (~
|
|
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
|
}
|
package/src/server/tools.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tools.js — MCP tool registrations
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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 (
|
|
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 (
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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} (
|
|
164
|
-
`Database: ${config.dbPath} (
|
|
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
|
|
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
|
);
|