context-vault 2.12.0 → 2.14.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 +59 -13
- package/assets/skills/compile-context/skill.md +45 -0
- package/bin/cli.js +548 -36
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/server/tools/clear-context.js +47 -0
- package/node_modules/@context-vault/core/src/server/tools/create-snapshot.js +222 -0
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +109 -0
- package/node_modules/@context-vault/core/src/server/tools.js +4 -0
- package/package.json +3 -2
- package/src/server/index.js +44 -0
package/README.md
CHANGED
|
@@ -40,19 +40,63 @@ Entries are organized by `kind` (insight, decision, pattern, reference, contact,
|
|
|
40
40
|
|
|
41
41
|
## CLI
|
|
42
42
|
|
|
43
|
-
| Command | Description
|
|
44
|
-
| ----------------------------- |
|
|
45
|
-
| `context-vault setup` | Interactive installer — detects tools, writes MCP configs
|
|
46
|
-
| `context-vault connect --key` | Connect AI tools to hosted vault
|
|
47
|
-
| `context-vault switch` | Switch between local and hosted MCP modes
|
|
48
|
-
| `context-vault serve` | Start the MCP server (used by AI clients)
|
|
49
|
-
| `context-vault status` | Vault health, paths, entry counts
|
|
50
|
-
| `context-vault
|
|
51
|
-
| `context-vault
|
|
52
|
-
| `context-vault
|
|
53
|
-
| `context-vault
|
|
54
|
-
| `context-vault
|
|
55
|
-
| `context-vault
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| ----------------------------- | ---------------------------------------------------------- |
|
|
45
|
+
| `context-vault setup` | Interactive installer — detects tools, writes MCP configs |
|
|
46
|
+
| `context-vault connect --key` | Connect AI tools to hosted vault |
|
|
47
|
+
| `context-vault switch` | Switch between local and hosted MCP modes |
|
|
48
|
+
| `context-vault serve` | Start the MCP server (used by AI clients) |
|
|
49
|
+
| `context-vault status` | Vault health, paths, entry counts |
|
|
50
|
+
| `context-vault flush` | Confirm DB is accessible; prints entry count and last save |
|
|
51
|
+
| `context-vault hooks install` | Install Claude Code memory and optional session flush hook |
|
|
52
|
+
| `context-vault hooks remove` | Remove the recall and session flush hooks |
|
|
53
|
+
| `context-vault reindex` | Rebuild search index |
|
|
54
|
+
| `context-vault import <path>` | Import .md, .csv, .json, .txt |
|
|
55
|
+
| `context-vault export` | Export to JSON or CSV |
|
|
56
|
+
| `context-vault ingest <url>` | Fetch URL and save as vault entry |
|
|
57
|
+
| `context-vault update` | Check for updates |
|
|
58
|
+
| `context-vault uninstall` | Remove MCP configs |
|
|
59
|
+
|
|
60
|
+
## Claude Code Lifecycle Hooks
|
|
61
|
+
|
|
62
|
+
Claude Code exposes shell hooks that fire on session events. context-vault integrates with two of them:
|
|
63
|
+
|
|
64
|
+
**UserPromptSubmit** — runs `context-vault recall` on every prompt, injecting relevant vault entries as context (installed via `hooks install`).
|
|
65
|
+
|
|
66
|
+
**SessionEnd** — runs `context-vault flush` when a session ends, confirming the vault is healthy and logging the current entry count. Install it when prompted by `hooks install`, or add it manually to `~/.claude/settings.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"hooks": {
|
|
71
|
+
"SessionEnd": [
|
|
72
|
+
{
|
|
73
|
+
"hooks": [
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "npx context-vault flush",
|
|
77
|
+
"timeout": 10
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `flush` command reads the DB, prints a one-line status (`context-vault ok — N entries, last save: <timestamp>`), and exits 0. It is intentionally a no-op write — its purpose is to confirm reachability at session boundaries.
|
|
87
|
+
|
|
88
|
+
To install both hooks at once:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
context-vault hooks install
|
|
92
|
+
# Follow the second prompt: "Install session auto-flush hook? (y/N)"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
To remove both hooks:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
context-vault hooks remove
|
|
99
|
+
```
|
|
56
100
|
|
|
57
101
|
## Manual MCP Config
|
|
58
102
|
|
|
@@ -73,6 +117,8 @@ If you prefer manual setup over `context-vault setup`:
|
|
|
73
117
|
|
|
74
118
|
No Node.js required — sign up at [app.context-vault.com](https://app.context-vault.com), get an API key, connect in 2 minutes.
|
|
75
119
|
|
|
120
|
+
Full setup instructions for Claude Code, Cursor, and GPT Actions: [docs/distribution/connect-in-2-minutes.md](../../docs/distribution/connect-in-2-minutes.md)
|
|
121
|
+
|
|
76
122
|
## Troubleshooting
|
|
77
123
|
|
|
78
124
|
**Install fails (native modules):**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compile-context
|
|
3
|
+
description: >
|
|
4
|
+
Compiles scattered vault entries on a topic into a single authoritative brief
|
|
5
|
+
for isolated retrieval in a fresh context window. Use when starting a new work
|
|
6
|
+
session on a project, preparing a handoff, or loading focused context without
|
|
7
|
+
noise. Also audits for stale or contradicting entries.
|
|
8
|
+
Triggers: "compile context", "create a brief", "context snapshot", "context bucket",
|
|
9
|
+
"make a brief for X", "load context for X".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# compile-context skill
|
|
13
|
+
|
|
14
|
+
When the user asks to compile context or create a brief for a topic, call `create_snapshot` to synthesize a context brief from the vault.
|
|
15
|
+
|
|
16
|
+
## Step 1 — Identify the topic
|
|
17
|
+
|
|
18
|
+
If the user provided a topic or project name, use it. If not, ask:
|
|
19
|
+
|
|
20
|
+
> "What topic or project should I compile context for?"
|
|
21
|
+
|
|
22
|
+
Derive a slug: lowercase, hyphens, no spaces (e.g. `neonode`, `context-vault`, `klarhimmel-infra`).
|
|
23
|
+
|
|
24
|
+
## Step 2 — Call create_snapshot
|
|
25
|
+
|
|
26
|
+
Call `create_snapshot` with:
|
|
27
|
+
|
|
28
|
+
- `topic`: the topic name the user provided
|
|
29
|
+
- `identity_key`: `snapshot-<slug>` (e.g. `snapshot-context-vault`)
|
|
30
|
+
- `tags` (optional): any relevant tags the user mentions
|
|
31
|
+
- `kinds` (optional): restrict to specific entry kinds if the user requests it
|
|
32
|
+
|
|
33
|
+
The tool handles retrieval, deduplication, LLM synthesis, and saving automatically.
|
|
34
|
+
|
|
35
|
+
## Step 3 — Report
|
|
36
|
+
|
|
37
|
+
After the tool returns, tell the user:
|
|
38
|
+
|
|
39
|
+
- The ULID of the saved brief
|
|
40
|
+
- How many entries were synthesized
|
|
41
|
+
- The exact call to retrieve it in a future session:
|
|
42
|
+
```
|
|
43
|
+
get_context(identity_key: "snapshot-<slug>")
|
|
44
|
+
```
|
|
45
|
+
- Suggest pinning the identity key in the relevant CLAUDE.md or MEMORY.md for zero-cost retrieval in fresh windows.
|
package/bin/cli.js
CHANGED
|
@@ -235,11 +235,15 @@ ${bold("Commands:")}
|
|
|
235
235
|
${cyan("connect")} --key cv_... Connect AI tools to hosted vault
|
|
236
236
|
${cyan("switch")} local|hosted Switch between local and hosted MCP modes
|
|
237
237
|
${cyan("serve")} Start the MCP server (used by AI clients)
|
|
238
|
-
${cyan("hooks")} install|
|
|
238
|
+
${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
|
|
239
|
+
${cyan("claude")} install|uninstall Alias for hooks install|uninstall
|
|
240
|
+
${cyan("skills")} install Install bundled Claude Code skills
|
|
241
|
+
${cyan("flush")} Check vault health and confirm DB is accessible
|
|
239
242
|
${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
|
|
240
243
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
241
244
|
${cyan("prune")} Remove expired entries (use --dry-run to preview)
|
|
242
245
|
${cyan("status")} Show vault diagnostics
|
|
246
|
+
${cyan("doctor")} Diagnose and repair common issues
|
|
243
247
|
${cyan("update")} Check for and install updates
|
|
244
248
|
${cyan("uninstall")} Remove MCP configs and optionally data
|
|
245
249
|
${cyan("import")} <path> Import entries from file or directory
|
|
@@ -729,6 +733,37 @@ async function runSetup() {
|
|
|
729
733
|
}
|
|
730
734
|
}
|
|
731
735
|
|
|
736
|
+
// Claude Code skills (opt-in)
|
|
737
|
+
if (claudeConfigured && !isNonInteractive) {
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(dim(" Install Claude Code skills? (recommended)"));
|
|
740
|
+
console.log(
|
|
741
|
+
dim(" compile-context — compile vault entries into a project brief"),
|
|
742
|
+
);
|
|
743
|
+
console.log();
|
|
744
|
+
const skillAnswer = await prompt(
|
|
745
|
+
" Install Claude Code skills? (Y/n):",
|
|
746
|
+
"Y",
|
|
747
|
+
);
|
|
748
|
+
const installSkillsFlag = skillAnswer.toLowerCase() !== "n";
|
|
749
|
+
if (installSkillsFlag) {
|
|
750
|
+
try {
|
|
751
|
+
const names = installSkills();
|
|
752
|
+
if (names.length > 0) {
|
|
753
|
+
for (const name of names) {
|
|
754
|
+
console.log(`\n ${green("+")} ${name} skill installed`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
console.log(`\n ${red("x")} Skills install failed: ${e.message}`);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
console.log(
|
|
762
|
+
dim(` Skipped — install later: context-vault skills install`),
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
732
767
|
// Seed entry
|
|
733
768
|
const seeded = createSeedEntries(resolvedVaultDir);
|
|
734
769
|
if (seeded > 0) {
|
|
@@ -1484,6 +1519,8 @@ async function runStatus() {
|
|
|
1484
1519
|
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1485
1520
|
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
1486
1521
|
const { gatherVaultStatus } = await import("@context-vault/core/core/status");
|
|
1522
|
+
const { errorLogPath, errorLogCount } =
|
|
1523
|
+
await import("@context-vault/core/core/error-log");
|
|
1487
1524
|
|
|
1488
1525
|
const config = resolveConfig();
|
|
1489
1526
|
|
|
@@ -1566,6 +1603,18 @@ async function runStatus() {
|
|
|
1566
1603
|
console.log(yellow(" Stale paths detected in DB."));
|
|
1567
1604
|
console.log(` Run ${cyan("context-vault reindex")} to update.`);
|
|
1568
1605
|
}
|
|
1606
|
+
|
|
1607
|
+
const logCount = errorLogCount(config.dataDir);
|
|
1608
|
+
if (logCount > 0) {
|
|
1609
|
+
const logPath = errorLogPath(config.dataDir);
|
|
1610
|
+
console.log();
|
|
1611
|
+
console.log(
|
|
1612
|
+
yellow(
|
|
1613
|
+
` ${logCount} startup error${logCount === 1 ? "" : "s"} logged — run ${cyan("context-vault doctor")} for details`,
|
|
1614
|
+
),
|
|
1615
|
+
);
|
|
1616
|
+
console.log(` ${dim(logPath)}`);
|
|
1617
|
+
}
|
|
1569
1618
|
console.log();
|
|
1570
1619
|
}
|
|
1571
1620
|
|
|
@@ -2110,16 +2159,25 @@ async function runRecall() {
|
|
|
2110
2159
|
const results = await hybridSearch(ctx, query, { limit: 5 });
|
|
2111
2160
|
if (!results.length) return;
|
|
2112
2161
|
|
|
2113
|
-
const
|
|
2162
|
+
const MAX_TOTAL = 2000;
|
|
2163
|
+
const ENTRY_BODY_LIMIT = 400;
|
|
2164
|
+
const entries = [];
|
|
2165
|
+
let totalChars = 0;
|
|
2166
|
+
|
|
2114
2167
|
for (const r of results) {
|
|
2115
2168
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2169
|
+
const tagsAttr = entryTags.length ? ` tags="${entryTags.join(",")}"` : "";
|
|
2170
|
+
const body = r.body?.slice(0, ENTRY_BODY_LIMIT) ?? "";
|
|
2171
|
+
const entry = `<entry kind="${r.kind || "knowledge"}"${tagsAttr}>\n${body}\n</entry>`;
|
|
2172
|
+
if (totalChars + entry.length > MAX_TOTAL) break;
|
|
2173
|
+
entries.push(entry);
|
|
2174
|
+
totalChars += entry.length;
|
|
2120
2175
|
}
|
|
2121
2176
|
|
|
2122
|
-
|
|
2177
|
+
if (!entries.length) return;
|
|
2178
|
+
|
|
2179
|
+
const block = `<context-vault>\n${entries.join("\n")}\n</context-vault>\n`;
|
|
2180
|
+
process.stdout.write(block);
|
|
2123
2181
|
} catch {
|
|
2124
2182
|
// fail silently — never interrupt the user's workflow
|
|
2125
2183
|
} finally {
|
|
@@ -2129,6 +2187,69 @@ async function runRecall() {
|
|
|
2129
2187
|
}
|
|
2130
2188
|
}
|
|
2131
2189
|
|
|
2190
|
+
async function runFlush() {
|
|
2191
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2192
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2193
|
+
|
|
2194
|
+
let db;
|
|
2195
|
+
try {
|
|
2196
|
+
const config = resolveConfig();
|
|
2197
|
+
db = await initDatabase(config.dbPath);
|
|
2198
|
+
|
|
2199
|
+
const { c: entryCount } = db
|
|
2200
|
+
.prepare("SELECT COUNT(*) as c FROM vault")
|
|
2201
|
+
.get();
|
|
2202
|
+
|
|
2203
|
+
const lastSaveRow = db
|
|
2204
|
+
.prepare("SELECT MAX(COALESCE(updated_at, created_at)) as ts FROM vault")
|
|
2205
|
+
.get();
|
|
2206
|
+
const lastSave = lastSaveRow?.ts ?? "n/a";
|
|
2207
|
+
|
|
2208
|
+
console.log(
|
|
2209
|
+
`context-vault ok — ${entryCount} ${entryCount === 1 ? "entry" : "entries"}, last save: ${lastSave}`,
|
|
2210
|
+
);
|
|
2211
|
+
} catch (e) {
|
|
2212
|
+
console.error(red(`context-vault flush failed: ${e.message}`));
|
|
2213
|
+
process.exit(1);
|
|
2214
|
+
} finally {
|
|
2215
|
+
try {
|
|
2216
|
+
db?.close();
|
|
2217
|
+
} catch {}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/**
|
|
2222
|
+
* Copies all skills from the bundled assets/skills/ directory into ~/.claude/skills/.
|
|
2223
|
+
* Returns an array of installed skill names.
|
|
2224
|
+
*/
|
|
2225
|
+
function installSkills() {
|
|
2226
|
+
const assetsSkillsDir = join(ROOT, "assets", "skills");
|
|
2227
|
+
const targetDir = join(HOME, ".claude", "skills");
|
|
2228
|
+
|
|
2229
|
+
if (!existsSync(assetsSkillsDir)) return [];
|
|
2230
|
+
|
|
2231
|
+
const skillNames = readdirSync(assetsSkillsDir).filter((name) => {
|
|
2232
|
+
try {
|
|
2233
|
+
return statSync(join(assetsSkillsDir, name)).isDirectory();
|
|
2234
|
+
} catch {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
const installed = [];
|
|
2240
|
+
for (const skillName of skillNames) {
|
|
2241
|
+
const srcDir = join(assetsSkillsDir, skillName);
|
|
2242
|
+
const destDir = join(targetDir, skillName);
|
|
2243
|
+
mkdirSync(destDir, { recursive: true });
|
|
2244
|
+
const files = readdirSync(srcDir);
|
|
2245
|
+
for (const file of files) {
|
|
2246
|
+
copyFileSync(join(srcDir, file), join(destDir, file));
|
|
2247
|
+
}
|
|
2248
|
+
installed.push(skillName);
|
|
2249
|
+
}
|
|
2250
|
+
return installed;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2132
2253
|
/** Returns the path to Claude Code's global settings.json */
|
|
2133
2254
|
function claudeSettingsPath() {
|
|
2134
2255
|
return join(HOME, ".claude", "settings.json");
|
|
@@ -2176,6 +2297,76 @@ function installClaudeHook() {
|
|
|
2176
2297
|
return true;
|
|
2177
2298
|
}
|
|
2178
2299
|
|
|
2300
|
+
/**
|
|
2301
|
+
* Writes a SessionEnd hook entry for context-vault flush to ~/.claude/settings.json.
|
|
2302
|
+
* Returns true if installed, false if already present.
|
|
2303
|
+
*/
|
|
2304
|
+
function installSessionEndHook() {
|
|
2305
|
+
const settingsPath = claudeSettingsPath();
|
|
2306
|
+
let settings = {};
|
|
2307
|
+
|
|
2308
|
+
if (existsSync(settingsPath)) {
|
|
2309
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
2310
|
+
try {
|
|
2311
|
+
settings = JSON.parse(raw);
|
|
2312
|
+
} catch {
|
|
2313
|
+
const bak = settingsPath + ".bak";
|
|
2314
|
+
copyFileSync(settingsPath, bak);
|
|
2315
|
+
console.log(yellow(` Backed up corrupted settings to ${bak}`));
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2320
|
+
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
2321
|
+
|
|
2322
|
+
const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
|
|
2323
|
+
h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2324
|
+
);
|
|
2325
|
+
if (alreadyInstalled) return false;
|
|
2326
|
+
|
|
2327
|
+
settings.hooks.SessionEnd.push({
|
|
2328
|
+
hooks: [
|
|
2329
|
+
{
|
|
2330
|
+
type: "command",
|
|
2331
|
+
command: "npx context-vault flush",
|
|
2332
|
+
timeout: 10,
|
|
2333
|
+
},
|
|
2334
|
+
],
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
2338
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* Removes the context-vault flush SessionEnd hook from ~/.claude/settings.json.
|
|
2344
|
+
* Returns true if removed, false if not found.
|
|
2345
|
+
*/
|
|
2346
|
+
function removeSessionEndHook() {
|
|
2347
|
+
const settingsPath = claudeSettingsPath();
|
|
2348
|
+
if (!existsSync(settingsPath)) return false;
|
|
2349
|
+
|
|
2350
|
+
let settings;
|
|
2351
|
+
try {
|
|
2352
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
2353
|
+
} catch {
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
if (!settings.hooks?.SessionEnd) return false;
|
|
2358
|
+
|
|
2359
|
+
const before = settings.hooks.SessionEnd.length;
|
|
2360
|
+
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
2361
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2362
|
+
);
|
|
2363
|
+
|
|
2364
|
+
if (settings.hooks.SessionEnd.length === before) return false;
|
|
2365
|
+
|
|
2366
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2367
|
+
return true;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2179
2370
|
/**
|
|
2180
2371
|
* Removes the context-vault recall hook from ~/.claude/settings.json.
|
|
2181
2372
|
* Returns true if removed, false if not found.
|
|
@@ -2204,62 +2395,371 @@ function removeClaudeHook() {
|
|
|
2204
2395
|
return true;
|
|
2205
2396
|
}
|
|
2206
2397
|
|
|
2207
|
-
async function
|
|
2398
|
+
async function runSkills() {
|
|
2208
2399
|
const sub = args[1];
|
|
2209
2400
|
|
|
2210
2401
|
if (sub === "install") {
|
|
2402
|
+
console.log();
|
|
2211
2403
|
try {
|
|
2212
|
-
const
|
|
2213
|
-
if (
|
|
2214
|
-
console.log(
|
|
2215
|
-
console.log(
|
|
2216
|
-
dim(
|
|
2217
|
-
" On every prompt, context-vault searches your vault for relevant entries",
|
|
2218
|
-
),
|
|
2219
|
-
);
|
|
2220
|
-
console.log(
|
|
2221
|
-
dim(
|
|
2222
|
-
" and injects them as additional context alongside Claude's native memory.",
|
|
2223
|
-
),
|
|
2224
|
-
);
|
|
2225
|
-
console.log(
|
|
2226
|
-
dim(`\n To remove: ${cyan("context-vault hooks remove")}`),
|
|
2227
|
-
);
|
|
2404
|
+
const names = installSkills();
|
|
2405
|
+
if (names.length === 0) {
|
|
2406
|
+
console.log(` ${yellow("!")} No bundled skills found.\n`);
|
|
2228
2407
|
} else {
|
|
2229
|
-
|
|
2408
|
+
for (const name of names) {
|
|
2409
|
+
console.log(
|
|
2410
|
+
` ${green("+")} ${name} — installed to ~/.claude/skills/${name}/`,
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
console.log();
|
|
2414
|
+
console.log(dim(" Skills are active immediately in Claude Code."));
|
|
2415
|
+
console.log(dim(` Trigger with: /${names.join(", /")}`));
|
|
2230
2416
|
}
|
|
2231
2417
|
} catch (e) {
|
|
2232
|
-
console.error(
|
|
2418
|
+
console.error(` ${red("x")} Skills install failed: ${e.message}\n`);
|
|
2233
2419
|
process.exit(1);
|
|
2234
2420
|
}
|
|
2235
2421
|
console.log();
|
|
2236
|
-
} else
|
|
2422
|
+
} else {
|
|
2423
|
+
console.log(`
|
|
2424
|
+
${bold("context-vault skills")} <install>
|
|
2425
|
+
|
|
2426
|
+
Manage bundled Claude Code skills.
|
|
2427
|
+
|
|
2428
|
+
${bold("Commands:")}
|
|
2429
|
+
${cyan("skills install")} Copy bundled skills into ~/.claude/skills/
|
|
2430
|
+
|
|
2431
|
+
${bold("Bundled skills:")}
|
|
2432
|
+
${cyan("compile-context")} Compile vault entries into a project brief using create_snapshot
|
|
2433
|
+
`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
async function runHooksInstall() {
|
|
2438
|
+
try {
|
|
2439
|
+
const installed = installClaudeHook();
|
|
2440
|
+
if (installed) {
|
|
2441
|
+
console.log(
|
|
2442
|
+
`\n ${green("✓")} Hook installed. Context vault will inject relevant entries on every prompt.\n`,
|
|
2443
|
+
);
|
|
2444
|
+
console.log(
|
|
2445
|
+
dim(
|
|
2446
|
+
" On every prompt, context-vault searches your vault for relevant entries",
|
|
2447
|
+
),
|
|
2448
|
+
);
|
|
2449
|
+
console.log(
|
|
2450
|
+
dim(
|
|
2451
|
+
" and injects them as a <context-vault> block before Claude sees your message.",
|
|
2452
|
+
),
|
|
2453
|
+
);
|
|
2454
|
+
console.log(
|
|
2455
|
+
dim(`\n To remove: ${cyan("context-vault hooks uninstall")}`),
|
|
2456
|
+
);
|
|
2457
|
+
} else {
|
|
2458
|
+
console.log(`\n ${yellow("!")} Hook already installed.\n`);
|
|
2459
|
+
}
|
|
2460
|
+
} catch (e) {
|
|
2461
|
+
console.error(`\n ${red("x")} Failed to install hook: ${e.message}\n`);
|
|
2462
|
+
process.exit(1);
|
|
2463
|
+
}
|
|
2464
|
+
console.log();
|
|
2465
|
+
|
|
2466
|
+
const installFlush =
|
|
2467
|
+
flags.has("--flush") ||
|
|
2468
|
+
(await prompt(
|
|
2469
|
+
" Install SessionEnd flush hook? (saves vault health summary at session end) (y/N):",
|
|
2470
|
+
"n",
|
|
2471
|
+
));
|
|
2472
|
+
const shouldInstallFlush =
|
|
2473
|
+
installFlush === true ||
|
|
2474
|
+
(typeof installFlush === "string" &&
|
|
2475
|
+
installFlush.toLowerCase().startsWith("y"));
|
|
2476
|
+
|
|
2477
|
+
if (shouldInstallFlush) {
|
|
2237
2478
|
try {
|
|
2238
|
-
const
|
|
2239
|
-
if (
|
|
2240
|
-
console.log(`\n ${green("✓")}
|
|
2479
|
+
const flushInstalled = installSessionEndHook();
|
|
2480
|
+
if (flushInstalled) {
|
|
2481
|
+
console.log(`\n ${green("✓")} SessionEnd flush hook installed.\n`);
|
|
2482
|
+
console.log(
|
|
2483
|
+
dim(
|
|
2484
|
+
" At the end of each session, context-vault flush confirms the vault is healthy.",
|
|
2485
|
+
),
|
|
2486
|
+
);
|
|
2241
2487
|
} else {
|
|
2242
|
-
console.log(
|
|
2488
|
+
console.log(
|
|
2489
|
+
`\n ${yellow("!")} SessionEnd flush hook already installed.\n`,
|
|
2490
|
+
);
|
|
2243
2491
|
}
|
|
2244
2492
|
} catch (e) {
|
|
2245
|
-
console.error(
|
|
2493
|
+
console.error(
|
|
2494
|
+
`\n ${red("x")} Failed to install session flush hook: ${e.message}\n`,
|
|
2495
|
+
);
|
|
2246
2496
|
process.exit(1);
|
|
2247
2497
|
}
|
|
2498
|
+
console.log();
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
async function runHooksUninstall() {
|
|
2503
|
+
try {
|
|
2504
|
+
const removed = removeClaudeHook();
|
|
2505
|
+
if (removed) {
|
|
2506
|
+
console.log(`\n ${green("✓")} Claude Code memory hook removed.\n`);
|
|
2507
|
+
} else {
|
|
2508
|
+
console.log(`\n ${yellow("!")} Hook not found — nothing to remove.\n`);
|
|
2509
|
+
}
|
|
2510
|
+
} catch (e) {
|
|
2511
|
+
console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
|
|
2512
|
+
process.exit(1);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
try {
|
|
2516
|
+
const flushRemoved = removeSessionEndHook();
|
|
2517
|
+
if (flushRemoved) {
|
|
2518
|
+
console.log(`\n ${green("✓")} SessionEnd flush hook removed.\n`);
|
|
2519
|
+
}
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
console.error(
|
|
2522
|
+
`\n ${red("x")} Failed to remove session flush hook: ${e.message}\n`,
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
async function runHooks() {
|
|
2528
|
+
const sub = args[1];
|
|
2529
|
+
|
|
2530
|
+
if (sub === "install") {
|
|
2531
|
+
await runHooksInstall();
|
|
2532
|
+
} else if (sub === "remove" || sub === "uninstall") {
|
|
2533
|
+
await runHooksUninstall();
|
|
2248
2534
|
} else {
|
|
2249
2535
|
console.log(`
|
|
2250
|
-
${bold("context-vault hooks")} <install|
|
|
2536
|
+
${bold("context-vault hooks")} <install|uninstall>
|
|
2251
2537
|
|
|
2252
2538
|
Manage the Claude Code memory hook integration.
|
|
2253
2539
|
When installed, context-vault automatically searches your vault on every user
|
|
2254
|
-
prompt and injects relevant entries as
|
|
2540
|
+
prompt and injects relevant entries as a <context-vault> XML block.
|
|
2255
2541
|
|
|
2256
2542
|
${bold("Commands:")}
|
|
2257
|
-
${cyan("hooks install")}
|
|
2258
|
-
|
|
2543
|
+
${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2544
|
+
Also prompts to install a SessionEnd flush hook
|
|
2545
|
+
${cyan("hooks uninstall")} Remove the recall hook and SessionEnd flush hook
|
|
2259
2546
|
`);
|
|
2260
2547
|
}
|
|
2261
2548
|
}
|
|
2262
2549
|
|
|
2550
|
+
async function runClaude() {
|
|
2551
|
+
const sub = args[1];
|
|
2552
|
+
|
|
2553
|
+
if (sub === "install") {
|
|
2554
|
+
await runHooksInstall();
|
|
2555
|
+
} else if (sub === "uninstall" || sub === "remove") {
|
|
2556
|
+
await runHooksUninstall();
|
|
2557
|
+
} else {
|
|
2558
|
+
console.log(`
|
|
2559
|
+
${bold("context-vault claude")} <install|uninstall>
|
|
2560
|
+
|
|
2561
|
+
Manage the Claude Code memory hook integration.
|
|
2562
|
+
Alias for ${cyan("context-vault hooks install|uninstall")}.
|
|
2563
|
+
|
|
2564
|
+
${bold("Commands:")}
|
|
2565
|
+
${cyan("claude install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2566
|
+
${cyan("claude uninstall")} Remove the recall hook and SessionEnd flush hook
|
|
2567
|
+
`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
async function runDoctor() {
|
|
2572
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2573
|
+
const { errorLogPath, errorLogCount } =
|
|
2574
|
+
await import("@context-vault/core/core/error-log");
|
|
2575
|
+
|
|
2576
|
+
console.log();
|
|
2577
|
+
console.log(` ${bold("◇ context-vault doctor")} ${dim(`v${VERSION}`)}`);
|
|
2578
|
+
console.log();
|
|
2579
|
+
|
|
2580
|
+
let allOk = true;
|
|
2581
|
+
|
|
2582
|
+
// ── Node.js version ──────────────────────────────────────────────────────
|
|
2583
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
2584
|
+
if (nodeMajor < 20) {
|
|
2585
|
+
console.log(
|
|
2586
|
+
` ${red("✘")} Node.js ${process.versions.node} — requires >= 20`,
|
|
2587
|
+
);
|
|
2588
|
+
console.log(
|
|
2589
|
+
` ${dim("Fix: install a newer Node.js from https://nodejs.org/")}`,
|
|
2590
|
+
);
|
|
2591
|
+
allOk = false;
|
|
2592
|
+
} else {
|
|
2593
|
+
console.log(
|
|
2594
|
+
` ${green("✓")} Node.js ${process.versions.node} ${dim(`(${process.execPath})`)}`,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
2599
|
+
let config;
|
|
2600
|
+
try {
|
|
2601
|
+
config = resolveConfig();
|
|
2602
|
+
const configExists = existsSync(config.configPath);
|
|
2603
|
+
console.log(
|
|
2604
|
+
` ${green("✓")} Config ${dim(`(${configExists ? "exists" : "using defaults"}: ${config.configPath})`)}`,
|
|
2605
|
+
);
|
|
2606
|
+
} catch (e) {
|
|
2607
|
+
console.log(` ${red("✘")} Config parse error: ${e.message}`);
|
|
2608
|
+
console.log(
|
|
2609
|
+
` ${dim(`Fix: delete or repair ${join(HOME, ".context-mcp", "config.json")}`)}`,
|
|
2610
|
+
);
|
|
2611
|
+
allOk = false;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
if (config) {
|
|
2615
|
+
// ── Data dir ───────────────────────────────────────────────────────────
|
|
2616
|
+
if (existsSync(config.dataDir)) {
|
|
2617
|
+
console.log(` ${green("✓")} Data dir ${dim(config.dataDir)}`);
|
|
2618
|
+
} else {
|
|
2619
|
+
console.log(
|
|
2620
|
+
` ${yellow("!")} Data dir missing — will be created on next start`,
|
|
2621
|
+
);
|
|
2622
|
+
console.log(` ${dim(`mkdir -p "${config.dataDir}"`)}`);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// ── Vault dir ─────────────────────────────────────────────────────────
|
|
2626
|
+
if (existsSync(config.vaultDir)) {
|
|
2627
|
+
try {
|
|
2628
|
+
const probe = join(config.vaultDir, ".write-probe");
|
|
2629
|
+
writeFileSync(probe, "");
|
|
2630
|
+
unlinkSync(probe);
|
|
2631
|
+
console.log(` ${green("✓")} Vault dir ${dim(config.vaultDir)}`);
|
|
2632
|
+
} catch {
|
|
2633
|
+
console.log(` ${red("✘")} Vault dir not writable: ${config.vaultDir}`);
|
|
2634
|
+
console.log(` ${dim(`Fix: chmod u+w "${config.vaultDir}"`)}`);
|
|
2635
|
+
allOk = false;
|
|
2636
|
+
}
|
|
2637
|
+
} else {
|
|
2638
|
+
console.log(
|
|
2639
|
+
` ${yellow("!")} Vault dir missing — will be created on next start`,
|
|
2640
|
+
);
|
|
2641
|
+
console.log(` ${dim(`mkdir -p "${config.vaultDir}"`)}`);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
2645
|
+
if (existsSync(config.dbPath)) {
|
|
2646
|
+
try {
|
|
2647
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2648
|
+
const db = await initDatabase(config.dbPath);
|
|
2649
|
+
db.close();
|
|
2650
|
+
console.log(` ${green("✓")} Database ${dim(config.dbPath)}`);
|
|
2651
|
+
} catch (e) {
|
|
2652
|
+
console.log(` ${red("✘")} Database error: ${e.message}`);
|
|
2653
|
+
console.log(
|
|
2654
|
+
` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
|
|
2655
|
+
);
|
|
2656
|
+
allOk = false;
|
|
2657
|
+
}
|
|
2658
|
+
} else {
|
|
2659
|
+
console.log(
|
|
2660
|
+
` ${yellow("!")} Database missing — will be created on next start`,
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// ── Launcher (server.mjs) ─────────────────────────────────────────────
|
|
2665
|
+
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
2666
|
+
if (existsSync(launcherPath)) {
|
|
2667
|
+
const launcherContent = readFileSync(launcherPath, "utf-8");
|
|
2668
|
+
const match = launcherContent.match(/import "(.+?)"/);
|
|
2669
|
+
if (match) {
|
|
2670
|
+
const serverEntryPath = match[1];
|
|
2671
|
+
if (existsSync(serverEntryPath)) {
|
|
2672
|
+
console.log(
|
|
2673
|
+
` ${green("✓")} Launcher ${dim(`→ ${serverEntryPath}`)}`,
|
|
2674
|
+
);
|
|
2675
|
+
} else {
|
|
2676
|
+
console.log(
|
|
2677
|
+
` ${red("✘")} Launcher points to missing server: ${serverEntryPath}`,
|
|
2678
|
+
);
|
|
2679
|
+
console.log(
|
|
2680
|
+
` ${dim("Fix: run context-vault setup to reinstall")}`,
|
|
2681
|
+
);
|
|
2682
|
+
allOk = false;
|
|
2683
|
+
}
|
|
2684
|
+
} else {
|
|
2685
|
+
console.log(` ${green("✓")} Launcher exists ${dim(launcherPath)}`);
|
|
2686
|
+
}
|
|
2687
|
+
} else {
|
|
2688
|
+
console.log(` ${yellow("!")} Launcher not found at ${launcherPath}`);
|
|
2689
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2690
|
+
allOk = false;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// ── Error log ─────────────────────────────────────────────────────────
|
|
2694
|
+
const logPath = errorLogPath(config.dataDir);
|
|
2695
|
+
const logCount = errorLogCount(config.dataDir);
|
|
2696
|
+
if (logCount > 0) {
|
|
2697
|
+
console.log();
|
|
2698
|
+
console.log(
|
|
2699
|
+
` ${yellow("!")} Error log has ${logCount} entr${logCount === 1 ? "y" : "ies"}: ${dim(logPath)}`,
|
|
2700
|
+
);
|
|
2701
|
+
try {
|
|
2702
|
+
const lines = readFileSync(logPath, "utf-8")
|
|
2703
|
+
.split("\n")
|
|
2704
|
+
.filter((l) => l.trim());
|
|
2705
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
2706
|
+
console.log(` Last error: ${red(last.message)}`);
|
|
2707
|
+
console.log(
|
|
2708
|
+
` Phase: ${dim(last.phase || "unknown")} Time: ${dim(last.timestamp)}`,
|
|
2709
|
+
);
|
|
2710
|
+
} catch {}
|
|
2711
|
+
console.log(` ${dim(`To clear: rm "${logPath}"`)}`);
|
|
2712
|
+
allOk = false;
|
|
2713
|
+
} else {
|
|
2714
|
+
console.log(` ${green("✓")} No startup errors logged`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// ── MCP tool configs ──────────────────────────────────────────────────────
|
|
2719
|
+
console.log();
|
|
2720
|
+
console.log(bold(" Tool Configurations"));
|
|
2721
|
+
const claudeConfigPath = join(HOME, ".claude.json");
|
|
2722
|
+
if (existsSync(claudeConfigPath)) {
|
|
2723
|
+
try {
|
|
2724
|
+
const claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
|
|
2725
|
+
const servers = claudeConfig?.mcpServers || {};
|
|
2726
|
+
if (servers["context-vault"]) {
|
|
2727
|
+
const srv = servers["context-vault"];
|
|
2728
|
+
const cmd = [srv.command, ...(srv.args || [])].join(" ");
|
|
2729
|
+
console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
|
|
2730
|
+
} else {
|
|
2731
|
+
console.log(` ${dim("-")} Claude Code: context-vault not configured`);
|
|
2732
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2733
|
+
}
|
|
2734
|
+
} catch {
|
|
2735
|
+
console.log(
|
|
2736
|
+
` ${yellow("!")} Claude Code: could not read ~/.claude.json`,
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
} else {
|
|
2740
|
+
console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
2744
|
+
console.log();
|
|
2745
|
+
if (allOk) {
|
|
2746
|
+
console.log(
|
|
2747
|
+
` ${green("✓ All checks passed.")} If the MCP server still fails, try:`,
|
|
2748
|
+
);
|
|
2749
|
+
console.log(
|
|
2750
|
+
` ${dim("context-vault setup")} — reconfigure tool integrations`,
|
|
2751
|
+
);
|
|
2752
|
+
} else {
|
|
2753
|
+
console.log(
|
|
2754
|
+
` ${yellow("Some issues found.")} Address the items above, then restart your AI tool.`,
|
|
2755
|
+
);
|
|
2756
|
+
console.log(
|
|
2757
|
+
` ${dim("context-vault setup")} — reconfigure and repair installation`,
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
console.log();
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2263
2763
|
async function runServe() {
|
|
2264
2764
|
await import("../src/server/index.js");
|
|
2265
2765
|
}
|
|
@@ -2291,6 +2791,15 @@ async function main() {
|
|
|
2291
2791
|
case "hooks":
|
|
2292
2792
|
await runHooks();
|
|
2293
2793
|
break;
|
|
2794
|
+
case "claude":
|
|
2795
|
+
await runClaude();
|
|
2796
|
+
break;
|
|
2797
|
+
case "skills":
|
|
2798
|
+
await runSkills();
|
|
2799
|
+
break;
|
|
2800
|
+
case "flush":
|
|
2801
|
+
await runFlush();
|
|
2802
|
+
break;
|
|
2294
2803
|
case "recall":
|
|
2295
2804
|
await runRecall();
|
|
2296
2805
|
break;
|
|
@@ -2321,6 +2830,9 @@ async function main() {
|
|
|
2321
2830
|
case "migrate":
|
|
2322
2831
|
await runMigrate();
|
|
2323
2832
|
break;
|
|
2833
|
+
case "doctor":
|
|
2834
|
+
await runDoctor();
|
|
2835
|
+
break;
|
|
2324
2836
|
default:
|
|
2325
2837
|
console.error(red(`Unknown command: ${command}`));
|
|
2326
2838
|
console.error(`Run ${cyan("context-vault --help")} for usage.`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
export const name = "clear_context";
|
|
5
|
+
|
|
6
|
+
export const description =
|
|
7
|
+
"Reset active in-memory session context without deleting vault entries. Call this when switching projects or topics mid-session. With `scope`, all subsequent get_context calls should filter to that tag/project. Vault data is never modified.";
|
|
8
|
+
|
|
9
|
+
export const inputSchema = {
|
|
10
|
+
scope: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Optional tag or project name to focus on going forward. When provided, treat subsequent get_context calls as if filtered to this tag.",
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} _ctx
|
|
21
|
+
*/
|
|
22
|
+
export function handler({ scope } = {}) {
|
|
23
|
+
const lines = [
|
|
24
|
+
"## Context Reset",
|
|
25
|
+
"",
|
|
26
|
+
"Active session context has been cleared. All previous context from this session should be disregarded.",
|
|
27
|
+
"",
|
|
28
|
+
"Vault entries are unchanged — no data was deleted.",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (scope?.trim()) {
|
|
32
|
+
const trimmed = scope.trim();
|
|
33
|
+
lines.push(
|
|
34
|
+
"",
|
|
35
|
+
`### Active Scope: \`${trimmed}\``,
|
|
36
|
+
"",
|
|
37
|
+
`Going forward, treat \`get_context\` calls as scoped to the tag or project **"${trimmed}"** unless the user explicitly requests a different scope or passes their own tag filters.`,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
lines.push(
|
|
41
|
+
"",
|
|
42
|
+
"No scope set. Use `get_context` normally — all vault entries are accessible.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ok(lines.join("\n"));
|
|
47
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { hybridSearch } from "../../retrieve/index.js";
|
|
3
|
+
import { captureAndIndex } from "../../capture/index.js";
|
|
4
|
+
import { normalizeKind } from "../../core/files.js";
|
|
5
|
+
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
6
|
+
|
|
7
|
+
const NOISE_KINDS = new Set(["prompt-history", "task-notification"]);
|
|
8
|
+
const SYNTHESIS_MODEL = "claude-haiku-4-5-20251001";
|
|
9
|
+
const MAX_ENTRIES_FOR_SYNTHESIS = 40;
|
|
10
|
+
const MAX_BODY_PER_ENTRY = 600;
|
|
11
|
+
|
|
12
|
+
export const name = "create_snapshot";
|
|
13
|
+
|
|
14
|
+
export const description =
|
|
15
|
+
"Pull all relevant vault entries matching a topic, run an LLM synthesis pass to deduplicate and structure them into a context brief, then save and return the brief's ULID. The brief is saved as kind: 'brief' with a deterministic identity_key for retrieval.";
|
|
16
|
+
|
|
17
|
+
export const inputSchema = {
|
|
18
|
+
topic: z.string().describe("The topic or project name to snapshot"),
|
|
19
|
+
tags: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Optional tag filters — entries must match at least one"),
|
|
23
|
+
kinds: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional kind filters to restrict which entry types are pulled"),
|
|
27
|
+
identity_key: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(
|
|
31
|
+
"Deterministic key for the saved brief (defaults to slugified topic). Use the same key to overwrite a previous snapshot.",
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function buildSynthesisPrompt(topic, entries) {
|
|
36
|
+
const entriesBlock = entries
|
|
37
|
+
.map((e, i) => {
|
|
38
|
+
const tags = e.tags ? JSON.parse(e.tags) : [];
|
|
39
|
+
const tagStr = tags.length ? tags.join(", ") : "none";
|
|
40
|
+
const body = e.body
|
|
41
|
+
? e.body.slice(0, MAX_BODY_PER_ENTRY) +
|
|
42
|
+
(e.body.length > MAX_BODY_PER_ENTRY ? "…" : "")
|
|
43
|
+
: "(no body)";
|
|
44
|
+
return [
|
|
45
|
+
`### Entry ${i + 1} [${e.kind}] id: ${e.id}`,
|
|
46
|
+
`tags: ${tagStr}`,
|
|
47
|
+
`updated: ${e.updated_at || e.created_at || "unknown"}`,
|
|
48
|
+
body,
|
|
49
|
+
].join("\n");
|
|
50
|
+
})
|
|
51
|
+
.join("\n\n");
|
|
52
|
+
|
|
53
|
+
return `You are a knowledge synthesis assistant. Given the following vault entries about "${topic}", produce a structured context brief.
|
|
54
|
+
|
|
55
|
+
Deduplicate overlapping information, resolve any contradictions (note them in Audit Notes), and organise the content into the sections below. Keep each section concise and actionable. Omit sections that have no relevant content.
|
|
56
|
+
|
|
57
|
+
Output ONLY the markdown document — no preamble, no explanation.
|
|
58
|
+
|
|
59
|
+
Required format:
|
|
60
|
+
# ${topic} — Context Brief
|
|
61
|
+
## Status
|
|
62
|
+
(current state of the topic)
|
|
63
|
+
## Key Decisions
|
|
64
|
+
(architectural or strategic decisions made)
|
|
65
|
+
## Patterns & Conventions
|
|
66
|
+
(recurring patterns, coding conventions, standards)
|
|
67
|
+
## Active Constraints
|
|
68
|
+
(known limitations, hard requirements, deadlines)
|
|
69
|
+
## Open Questions
|
|
70
|
+
(unresolved questions or areas needing investigation)
|
|
71
|
+
## Audit Notes
|
|
72
|
+
(contradictions detected, stale entries flagged with their ids)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
VAULT ENTRIES:
|
|
76
|
+
|
|
77
|
+
${entriesBlock}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function callLlm(prompt) {
|
|
81
|
+
const { Anthropic } = await import("@anthropic-ai/sdk");
|
|
82
|
+
const client = new Anthropic();
|
|
83
|
+
const message = await client.messages.create({
|
|
84
|
+
model: SYNTHESIS_MODEL,
|
|
85
|
+
max_tokens: 2048,
|
|
86
|
+
messages: [{ role: "user", content: prompt }],
|
|
87
|
+
});
|
|
88
|
+
const block = message.content.find((b) => b.type === "text");
|
|
89
|
+
if (!block) throw new Error("LLM returned no text content");
|
|
90
|
+
return block.text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slugifyTopic(topic) {
|
|
94
|
+
return topic
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
97
|
+
.replace(/^-+|-+$/g, "")
|
|
98
|
+
.slice(0, 120);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function handler(
|
|
102
|
+
{ topic, tags, kinds, identity_key },
|
|
103
|
+
ctx,
|
|
104
|
+
{ ensureIndexed },
|
|
105
|
+
) {
|
|
106
|
+
const { config } = ctx;
|
|
107
|
+
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
108
|
+
|
|
109
|
+
const vaultErr = ensureVaultExists(config);
|
|
110
|
+
if (vaultErr) return vaultErr;
|
|
111
|
+
|
|
112
|
+
if (!topic?.trim()) {
|
|
113
|
+
return err("Required: topic (non-empty string)", "INVALID_INPUT");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await ensureIndexed();
|
|
117
|
+
|
|
118
|
+
const normalizedKinds = kinds?.map(normalizeKind) ?? [];
|
|
119
|
+
|
|
120
|
+
let candidates = [];
|
|
121
|
+
|
|
122
|
+
if (normalizedKinds.length > 0) {
|
|
123
|
+
for (const kindFilter of normalizedKinds) {
|
|
124
|
+
const rows = await hybridSearch(ctx, topic, {
|
|
125
|
+
kindFilter,
|
|
126
|
+
limit: Math.ceil(MAX_ENTRIES_FOR_SYNTHESIS / normalizedKinds.length),
|
|
127
|
+
userIdFilter: userId,
|
|
128
|
+
includeSuperseeded: false,
|
|
129
|
+
});
|
|
130
|
+
candidates.push(...rows);
|
|
131
|
+
}
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
candidates = candidates.filter((r) => {
|
|
134
|
+
if (seen.has(r.id)) return false;
|
|
135
|
+
seen.add(r.id);
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
candidates = await hybridSearch(ctx, topic, {
|
|
140
|
+
limit: MAX_ENTRIES_FOR_SYNTHESIS,
|
|
141
|
+
userIdFilter: userId,
|
|
142
|
+
includeSuperseeded: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (tags?.length) {
|
|
147
|
+
candidates = candidates.filter((r) => {
|
|
148
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
149
|
+
return tags.some((t) => entryTags.includes(t));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const noiseIds = candidates
|
|
154
|
+
.filter((r) => NOISE_KINDS.has(r.kind))
|
|
155
|
+
.map((r) => r.id);
|
|
156
|
+
|
|
157
|
+
const synthesisEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
|
|
158
|
+
|
|
159
|
+
if (synthesisEntries.length === 0) {
|
|
160
|
+
return err(
|
|
161
|
+
`No entries found for topic "${topic}" to synthesize. Try a broader topic or different tags.`,
|
|
162
|
+
"NO_ENTRIES",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let briefBody;
|
|
167
|
+
try {
|
|
168
|
+
const prompt = buildSynthesisPrompt(topic, synthesisEntries);
|
|
169
|
+
briefBody = await callLlm(prompt);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return err(
|
|
172
|
+
`LLM synthesis failed: ${e.message}. Ensure ANTHROPIC_API_KEY is set.`,
|
|
173
|
+
"LLM_ERROR",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const effectiveIdentityKey =
|
|
178
|
+
identity_key ?? `snapshot-${slugifyTopic(topic)}`;
|
|
179
|
+
|
|
180
|
+
const briefTags = [
|
|
181
|
+
"snapshot",
|
|
182
|
+
...(tags ?? []),
|
|
183
|
+
...(normalizedKinds.length > 0 ? [] : []),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const supersedes = noiseIds.length > 0 ? noiseIds : undefined;
|
|
187
|
+
|
|
188
|
+
const entry = await captureAndIndex(ctx, {
|
|
189
|
+
kind: "brief",
|
|
190
|
+
title: `${topic} — Context Brief`,
|
|
191
|
+
body: briefBody,
|
|
192
|
+
tags: briefTags,
|
|
193
|
+
source: "create_snapshot",
|
|
194
|
+
identity_key: effectiveIdentityKey,
|
|
195
|
+
supersedes,
|
|
196
|
+
userId,
|
|
197
|
+
meta: {
|
|
198
|
+
topic,
|
|
199
|
+
entry_count: synthesisEntries.length,
|
|
200
|
+
noise_superseded: noiseIds.length,
|
|
201
|
+
synthesized_from: synthesisEntries.map((e) => e.id),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const parts = [
|
|
206
|
+
`✓ Snapshot created → id: ${entry.id}`,
|
|
207
|
+
` title: ${entry.title}`,
|
|
208
|
+
` identity_key: ${effectiveIdentityKey}`,
|
|
209
|
+
` synthesized from: ${synthesisEntries.length} entries`,
|
|
210
|
+
noiseIds.length > 0
|
|
211
|
+
? ` noise superseded: ${noiseIds.length} entries`
|
|
212
|
+
: null,
|
|
213
|
+
"",
|
|
214
|
+
"_Retrieve with: get_context(kind: 'brief', identity_key: '" +
|
|
215
|
+
effectiveIdentityKey +
|
|
216
|
+
"')_",
|
|
217
|
+
]
|
|
218
|
+
.filter((l) => l !== null)
|
|
219
|
+
.join("\n");
|
|
220
|
+
|
|
221
|
+
return ok(parts);
|
|
222
|
+
}
|
|
@@ -5,6 +5,87 @@ import { normalizeKind } from "../../core/files.js";
|
|
|
5
5
|
import { ok, err } from "../helpers.js";
|
|
6
6
|
import { isEmbedAvailable } from "../../index/embed.js";
|
|
7
7
|
|
|
8
|
+
const STALE_DUPLICATE_DAYS = 7;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect conflicts among a set of search result entries.
|
|
12
|
+
*
|
|
13
|
+
* Two checks are performed:
|
|
14
|
+
* 1. Supersession: if entry A's `superseded_by` points to any entry B in the
|
|
15
|
+
* result set, A is stale and should be discarded in favour of B.
|
|
16
|
+
* 2. Stale duplicate: two entries share the same kind and at least one common
|
|
17
|
+
* tag, but their `updated_at` timestamps differ by more than
|
|
18
|
+
* STALE_DUPLICATE_DAYS days — suggesting the older one may be outdated.
|
|
19
|
+
*
|
|
20
|
+
* No LLM calls, no new dependencies — pure in-memory set operations on the
|
|
21
|
+
* rows already fetched from the DB.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array} entries - Result rows (as returned by hybridSearch / filter-only mode)
|
|
24
|
+
* @param {import('../types.js').BaseCtx} _ctx - Unused for now; reserved for future DB look-ups
|
|
25
|
+
* @returns {Array<{entry_a_id: string, entry_b_id: string, reason: string, recommendation: string}>}
|
|
26
|
+
*/
|
|
27
|
+
export function detectConflicts(entries, _ctx) {
|
|
28
|
+
const conflicts = [];
|
|
29
|
+
const idSet = new Set(entries.map((e) => e.id));
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.superseded_by && idSet.has(entry.superseded_by)) {
|
|
33
|
+
conflicts.push({
|
|
34
|
+
entry_a_id: entry.id,
|
|
35
|
+
entry_b_id: entry.superseded_by,
|
|
36
|
+
reason: "superseded",
|
|
37
|
+
recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const supersededConflictPairs = new Set(
|
|
43
|
+
conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < entries.length; i++) {
|
|
47
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
48
|
+
const a = entries[i];
|
|
49
|
+
const b = entries[j];
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
supersededConflictPairs.has(`${a.id}|${b.id}`) ||
|
|
53
|
+
supersededConflictPairs.has(`${b.id}|${a.id}`)
|
|
54
|
+
) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (a.kind !== b.kind) continue;
|
|
59
|
+
|
|
60
|
+
const tagsA = a.tags ? JSON.parse(a.tags) : [];
|
|
61
|
+
const tagsB = b.tags ? JSON.parse(b.tags) : [];
|
|
62
|
+
|
|
63
|
+
if (!tagsA.length || !tagsB.length) continue;
|
|
64
|
+
|
|
65
|
+
const tagsSetA = new Set(tagsA);
|
|
66
|
+
const sharedTag = tagsB.some((t) => tagsSetA.has(t));
|
|
67
|
+
if (!sharedTag) continue;
|
|
68
|
+
|
|
69
|
+
const dateA = new Date(a.updated_at || a.created_at);
|
|
70
|
+
const dateB = new Date(b.updated_at || b.created_at);
|
|
71
|
+
if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) continue;
|
|
72
|
+
|
|
73
|
+
const diffDays = Math.abs(dateA - dateB) / 86400000;
|
|
74
|
+
if (diffDays <= STALE_DUPLICATE_DAYS) continue;
|
|
75
|
+
|
|
76
|
+
const [older, newer] = dateA < dateB ? [a, b] : [b, a];
|
|
77
|
+
conflicts.push({
|
|
78
|
+
entry_a_id: older.id,
|
|
79
|
+
entry_b_id: newer.id,
|
|
80
|
+
reason: "stale_duplicate",
|
|
81
|
+
recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return conflicts;
|
|
87
|
+
}
|
|
88
|
+
|
|
8
89
|
export const name = "get_context";
|
|
9
90
|
|
|
10
91
|
export const description =
|
|
@@ -48,6 +129,12 @@ export const inputSchema = {
|
|
|
48
129
|
.describe(
|
|
49
130
|
"If true, include entries that have been superseded by newer ones. Default: false.",
|
|
50
131
|
),
|
|
132
|
+
detect_conflicts: z
|
|
133
|
+
.boolean()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe(
|
|
136
|
+
"If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.",
|
|
137
|
+
),
|
|
51
138
|
};
|
|
52
139
|
|
|
53
140
|
/**
|
|
@@ -66,6 +153,7 @@ export async function handler(
|
|
|
66
153
|
until,
|
|
67
154
|
limit,
|
|
68
155
|
include_superseded,
|
|
156
|
+
detect_conflicts,
|
|
69
157
|
},
|
|
70
158
|
ctx,
|
|
71
159
|
{ ensureIndexed, reindexFailed },
|
|
@@ -227,6 +315,9 @@ export async function handler(
|
|
|
227
315
|
}
|
|
228
316
|
}
|
|
229
317
|
|
|
318
|
+
// Conflict detection
|
|
319
|
+
const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
|
|
320
|
+
|
|
230
321
|
const lines = [];
|
|
231
322
|
if (reindexFailed)
|
|
232
323
|
lines.push(
|
|
@@ -265,5 +356,23 @@ export async function handler(
|
|
|
265
356
|
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
266
357
|
lines.push("");
|
|
267
358
|
}
|
|
359
|
+
|
|
360
|
+
if (detect_conflicts) {
|
|
361
|
+
if (conflicts.length === 0) {
|
|
362
|
+
lines.push(
|
|
363
|
+
`## Conflict Detection\n\nNo conflicts detected among results.\n`,
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
|
|
367
|
+
for (const c of conflicts) {
|
|
368
|
+
lines.push(
|
|
369
|
+
`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
|
|
370
|
+
);
|
|
371
|
+
lines.push(` Recommendation: ${c.recommendation}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push("");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
268
377
|
return ok(lines.join("\n"));
|
|
269
378
|
}
|
|
@@ -11,6 +11,8 @@ import * as deleteContext from "./tools/delete-context.js";
|
|
|
11
11
|
import * as submitFeedback from "./tools/submit-feedback.js";
|
|
12
12
|
import * as ingestUrl from "./tools/ingest-url.js";
|
|
13
13
|
import * as contextStatus from "./tools/context-status.js";
|
|
14
|
+
import * as clearContext from "./tools/clear-context.js";
|
|
15
|
+
import * as createSnapshot from "./tools/create-snapshot.js";
|
|
14
16
|
|
|
15
17
|
const toolModules = [
|
|
16
18
|
getContext,
|
|
@@ -20,6 +22,8 @@ const toolModules = [
|
|
|
20
22
|
submitFeedback,
|
|
21
23
|
ingestUrl,
|
|
22
24
|
contextStatus,
|
|
25
|
+
clearContext,
|
|
26
|
+
createSnapshot,
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
const TOOL_TIMEOUT_MS = 60_000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"prepack": "node scripts/prepack.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
+
"assets/",
|
|
20
21
|
"bin/",
|
|
21
22
|
"src/",
|
|
22
23
|
"scripts/",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"@context-vault/core"
|
|
56
57
|
],
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@context-vault/core": "^2.
|
|
59
|
+
"@context-vault/core": "^2.14.0",
|
|
59
60
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
61
|
"sqlite-vec": "^0.1.0"
|
|
61
62
|
}
|
package/src/server/index.js
CHANGED
|
@@ -265,6 +265,50 @@ async function main() {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// ─── Top-level Safety Net ────────────────────────────────────────────────────
|
|
269
|
+
// Catch any errors that escape the main() try/catch (e.g. thrown in MCP
|
|
270
|
+
// transport callbacks or in unrelated async chains). Claude Code surfaces
|
|
271
|
+
// stderr when a server exits unexpectedly, so every message written here will
|
|
272
|
+
// be visible to the user.
|
|
273
|
+
|
|
274
|
+
process.on("uncaughtException", (err) => {
|
|
275
|
+
const dataDir = join(homedir(), ".context-mcp");
|
|
276
|
+
const logEntry = {
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
error_type: "uncaughtException",
|
|
279
|
+
message: err.message,
|
|
280
|
+
stack: err.stack?.split("\n").slice(0, 5).join(" | "),
|
|
281
|
+
node_version: process.version,
|
|
282
|
+
platform: process.platform,
|
|
283
|
+
arch: process.arch,
|
|
284
|
+
cv_version: pkg.version,
|
|
285
|
+
};
|
|
286
|
+
appendErrorLog(dataDir, logEntry);
|
|
287
|
+
console.error(`[context-vault] Uncaught exception: ${err.message}`);
|
|
288
|
+
console.error(`[context-vault] Error log: ${join(dataDir, "error.log")}`);
|
|
289
|
+
console.error(`[context-vault] Run: context-vault doctor`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
process.on("unhandledRejection", (reason) => {
|
|
294
|
+
const dataDir = join(homedir(), ".context-mcp");
|
|
295
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
296
|
+
const logEntry = {
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
error_type: "unhandledRejection",
|
|
299
|
+
message,
|
|
300
|
+
node_version: process.version,
|
|
301
|
+
platform: process.platform,
|
|
302
|
+
arch: process.arch,
|
|
303
|
+
cv_version: pkg.version,
|
|
304
|
+
};
|
|
305
|
+
appendErrorLog(dataDir, logEntry);
|
|
306
|
+
console.error(`[context-vault] Unhandled rejection: ${message}`);
|
|
307
|
+
console.error(`[context-vault] Error log: ${join(dataDir, "error.log")}`);
|
|
308
|
+
console.error(`[context-vault] Run: context-vault doctor`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
268
312
|
main().catch((err) => {
|
|
269
313
|
console.error(`[context-vault] Unexpected fatal error: ${err.message}`);
|
|
270
314
|
process.exit(1);
|