context-vault 2.12.0 → 2.13.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/bin/cli.js +368 -1
- 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/get-context.js +109 -0
- package/node_modules/@context-vault/core/src/server/tools.js +2 -0
- package/package.json +2 -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):**
|
package/bin/cli.js
CHANGED
|
@@ -236,10 +236,12 @@ ${bold("Commands:")}
|
|
|
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
238
|
${cyan("hooks")} install|remove Install or remove Claude Code memory hook
|
|
239
|
+
${cyan("flush")} Check vault health and confirm DB is accessible
|
|
239
240
|
${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
|
|
240
241
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
241
242
|
${cyan("prune")} Remove expired entries (use --dry-run to preview)
|
|
242
243
|
${cyan("status")} Show vault diagnostics
|
|
244
|
+
${cyan("doctor")} Diagnose and repair common issues
|
|
243
245
|
${cyan("update")} Check for and install updates
|
|
244
246
|
${cyan("uninstall")} Remove MCP configs and optionally data
|
|
245
247
|
${cyan("import")} <path> Import entries from file or directory
|
|
@@ -1484,6 +1486,8 @@ async function runStatus() {
|
|
|
1484
1486
|
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1485
1487
|
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
1486
1488
|
const { gatherVaultStatus } = await import("@context-vault/core/core/status");
|
|
1489
|
+
const { errorLogPath, errorLogCount } =
|
|
1490
|
+
await import("@context-vault/core/core/error-log");
|
|
1487
1491
|
|
|
1488
1492
|
const config = resolveConfig();
|
|
1489
1493
|
|
|
@@ -1566,6 +1570,18 @@ async function runStatus() {
|
|
|
1566
1570
|
console.log(yellow(" Stale paths detected in DB."));
|
|
1567
1571
|
console.log(` Run ${cyan("context-vault reindex")} to update.`);
|
|
1568
1572
|
}
|
|
1573
|
+
|
|
1574
|
+
const logCount = errorLogCount(config.dataDir);
|
|
1575
|
+
if (logCount > 0) {
|
|
1576
|
+
const logPath = errorLogPath(config.dataDir);
|
|
1577
|
+
console.log();
|
|
1578
|
+
console.log(
|
|
1579
|
+
yellow(
|
|
1580
|
+
` ${logCount} startup error${logCount === 1 ? "" : "s"} logged — run ${cyan("context-vault doctor")} for details`,
|
|
1581
|
+
),
|
|
1582
|
+
);
|
|
1583
|
+
console.log(` ${dim(logPath)}`);
|
|
1584
|
+
}
|
|
1569
1585
|
console.log();
|
|
1570
1586
|
}
|
|
1571
1587
|
|
|
@@ -2129,6 +2145,37 @@ async function runRecall() {
|
|
|
2129
2145
|
}
|
|
2130
2146
|
}
|
|
2131
2147
|
|
|
2148
|
+
async function runFlush() {
|
|
2149
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2150
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2151
|
+
|
|
2152
|
+
let db;
|
|
2153
|
+
try {
|
|
2154
|
+
const config = resolveConfig();
|
|
2155
|
+
db = await initDatabase(config.dbPath);
|
|
2156
|
+
|
|
2157
|
+
const { c: entryCount } = db
|
|
2158
|
+
.prepare("SELECT COUNT(*) as c FROM vault")
|
|
2159
|
+
.get();
|
|
2160
|
+
|
|
2161
|
+
const lastSaveRow = db
|
|
2162
|
+
.prepare("SELECT MAX(COALESCE(updated_at, created_at)) as ts FROM vault")
|
|
2163
|
+
.get();
|
|
2164
|
+
const lastSave = lastSaveRow?.ts ?? "n/a";
|
|
2165
|
+
|
|
2166
|
+
console.log(
|
|
2167
|
+
`context-vault ok — ${entryCount} ${entryCount === 1 ? "entry" : "entries"}, last save: ${lastSave}`,
|
|
2168
|
+
);
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
console.error(red(`context-vault flush failed: ${e.message}`));
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
} finally {
|
|
2173
|
+
try {
|
|
2174
|
+
db?.close();
|
|
2175
|
+
} catch {}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2132
2179
|
/** Returns the path to Claude Code's global settings.json */
|
|
2133
2180
|
function claudeSettingsPath() {
|
|
2134
2181
|
return join(HOME, ".claude", "settings.json");
|
|
@@ -2176,6 +2223,76 @@ function installClaudeHook() {
|
|
|
2176
2223
|
return true;
|
|
2177
2224
|
}
|
|
2178
2225
|
|
|
2226
|
+
/**
|
|
2227
|
+
* Writes a SessionEnd hook entry for context-vault flush to ~/.claude/settings.json.
|
|
2228
|
+
* Returns true if installed, false if already present.
|
|
2229
|
+
*/
|
|
2230
|
+
function installSessionEndHook() {
|
|
2231
|
+
const settingsPath = claudeSettingsPath();
|
|
2232
|
+
let settings = {};
|
|
2233
|
+
|
|
2234
|
+
if (existsSync(settingsPath)) {
|
|
2235
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
2236
|
+
try {
|
|
2237
|
+
settings = JSON.parse(raw);
|
|
2238
|
+
} catch {
|
|
2239
|
+
const bak = settingsPath + ".bak";
|
|
2240
|
+
copyFileSync(settingsPath, bak);
|
|
2241
|
+
console.log(yellow(` Backed up corrupted settings to ${bak}`));
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2246
|
+
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
2247
|
+
|
|
2248
|
+
const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
|
|
2249
|
+
h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2250
|
+
);
|
|
2251
|
+
if (alreadyInstalled) return false;
|
|
2252
|
+
|
|
2253
|
+
settings.hooks.SessionEnd.push({
|
|
2254
|
+
hooks: [
|
|
2255
|
+
{
|
|
2256
|
+
type: "command",
|
|
2257
|
+
command: "npx context-vault flush",
|
|
2258
|
+
timeout: 10,
|
|
2259
|
+
},
|
|
2260
|
+
],
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
2264
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* Removes the context-vault flush SessionEnd hook from ~/.claude/settings.json.
|
|
2270
|
+
* Returns true if removed, false if not found.
|
|
2271
|
+
*/
|
|
2272
|
+
function removeSessionEndHook() {
|
|
2273
|
+
const settingsPath = claudeSettingsPath();
|
|
2274
|
+
if (!existsSync(settingsPath)) return false;
|
|
2275
|
+
|
|
2276
|
+
let settings;
|
|
2277
|
+
try {
|
|
2278
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (!settings.hooks?.SessionEnd) return false;
|
|
2284
|
+
|
|
2285
|
+
const before = settings.hooks.SessionEnd.length;
|
|
2286
|
+
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
2287
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2288
|
+
);
|
|
2289
|
+
|
|
2290
|
+
if (settings.hooks.SessionEnd.length === before) return false;
|
|
2291
|
+
|
|
2292
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2179
2296
|
/**
|
|
2180
2297
|
* Removes the context-vault recall hook from ~/.claude/settings.json.
|
|
2181
2298
|
* Returns true if removed, false if not found.
|
|
@@ -2233,6 +2350,44 @@ async function runHooks() {
|
|
|
2233
2350
|
process.exit(1);
|
|
2234
2351
|
}
|
|
2235
2352
|
console.log();
|
|
2353
|
+
|
|
2354
|
+
// Prompt for optional session auto-flush (SessionEnd) hook
|
|
2355
|
+
const installFlush =
|
|
2356
|
+
flags.has("--flush") ||
|
|
2357
|
+
(await prompt(
|
|
2358
|
+
" Install session auto-flush hook? (runs context-vault flush at session end) (y/N):",
|
|
2359
|
+
"n",
|
|
2360
|
+
));
|
|
2361
|
+
const shouldInstallFlush =
|
|
2362
|
+
installFlush === true ||
|
|
2363
|
+
(typeof installFlush === "string" &&
|
|
2364
|
+
installFlush.toLowerCase().startsWith("y"));
|
|
2365
|
+
|
|
2366
|
+
if (shouldInstallFlush) {
|
|
2367
|
+
try {
|
|
2368
|
+
const flushInstalled = installSessionEndHook();
|
|
2369
|
+
if (flushInstalled) {
|
|
2370
|
+
console.log(
|
|
2371
|
+
`\n ${green("✓")} Session auto-flush hook installed (SessionEnd).\n`,
|
|
2372
|
+
);
|
|
2373
|
+
console.log(
|
|
2374
|
+
dim(
|
|
2375
|
+
" At the end of each session, context-vault flush confirms the vault is healthy.",
|
|
2376
|
+
),
|
|
2377
|
+
);
|
|
2378
|
+
} else {
|
|
2379
|
+
console.log(
|
|
2380
|
+
`\n ${yellow("!")} Session auto-flush hook already installed.\n`,
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
console.error(
|
|
2385
|
+
`\n ${red("x")} Failed to install session flush hook: ${e.message}\n`,
|
|
2386
|
+
);
|
|
2387
|
+
process.exit(1);
|
|
2388
|
+
}
|
|
2389
|
+
console.log();
|
|
2390
|
+
}
|
|
2236
2391
|
} else if (sub === "remove") {
|
|
2237
2392
|
try {
|
|
2238
2393
|
const removed = removeClaudeHook();
|
|
@@ -2245,6 +2400,19 @@ async function runHooks() {
|
|
|
2245
2400
|
console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
|
|
2246
2401
|
process.exit(1);
|
|
2247
2402
|
}
|
|
2403
|
+
|
|
2404
|
+
try {
|
|
2405
|
+
const flushRemoved = removeSessionEndHook();
|
|
2406
|
+
if (flushRemoved) {
|
|
2407
|
+
console.log(
|
|
2408
|
+
`\n ${green("✓")} Session auto-flush hook removed (SessionEnd).\n`,
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
} catch (e) {
|
|
2412
|
+
console.error(
|
|
2413
|
+
`\n ${red("x")} Failed to remove session flush hook: ${e.message}\n`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2248
2416
|
} else {
|
|
2249
2417
|
console.log(`
|
|
2250
2418
|
${bold("context-vault hooks")} <install|remove>
|
|
@@ -2255,11 +2423,204 @@ async function runHooks() {
|
|
|
2255
2423
|
|
|
2256
2424
|
${bold("Commands:")}
|
|
2257
2425
|
${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2258
|
-
|
|
2426
|
+
Also prompts to install a SessionEnd auto-flush hook
|
|
2427
|
+
${cyan("hooks remove")} Remove the recall hook and SessionEnd flush hook
|
|
2259
2428
|
`);
|
|
2260
2429
|
}
|
|
2261
2430
|
}
|
|
2262
2431
|
|
|
2432
|
+
async function runDoctor() {
|
|
2433
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2434
|
+
const { errorLogPath, errorLogCount } =
|
|
2435
|
+
await import("@context-vault/core/core/error-log");
|
|
2436
|
+
|
|
2437
|
+
console.log();
|
|
2438
|
+
console.log(` ${bold("◇ context-vault doctor")} ${dim(`v${VERSION}`)}`);
|
|
2439
|
+
console.log();
|
|
2440
|
+
|
|
2441
|
+
let allOk = true;
|
|
2442
|
+
|
|
2443
|
+
// ── Node.js version ──────────────────────────────────────────────────────
|
|
2444
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
2445
|
+
if (nodeMajor < 20) {
|
|
2446
|
+
console.log(
|
|
2447
|
+
` ${red("✘")} Node.js ${process.versions.node} — requires >= 20`,
|
|
2448
|
+
);
|
|
2449
|
+
console.log(
|
|
2450
|
+
` ${dim("Fix: install a newer Node.js from https://nodejs.org/")}`,
|
|
2451
|
+
);
|
|
2452
|
+
allOk = false;
|
|
2453
|
+
} else {
|
|
2454
|
+
console.log(
|
|
2455
|
+
` ${green("✓")} Node.js ${process.versions.node} ${dim(`(${process.execPath})`)}`,
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
2460
|
+
let config;
|
|
2461
|
+
try {
|
|
2462
|
+
config = resolveConfig();
|
|
2463
|
+
const configExists = existsSync(config.configPath);
|
|
2464
|
+
console.log(
|
|
2465
|
+
` ${green("✓")} Config ${dim(`(${configExists ? "exists" : "using defaults"}: ${config.configPath})`)}`,
|
|
2466
|
+
);
|
|
2467
|
+
} catch (e) {
|
|
2468
|
+
console.log(` ${red("✘")} Config parse error: ${e.message}`);
|
|
2469
|
+
console.log(
|
|
2470
|
+
` ${dim(`Fix: delete or repair ${join(HOME, ".context-mcp", "config.json")}`)}`,
|
|
2471
|
+
);
|
|
2472
|
+
allOk = false;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
if (config) {
|
|
2476
|
+
// ── Data dir ───────────────────────────────────────────────────────────
|
|
2477
|
+
if (existsSync(config.dataDir)) {
|
|
2478
|
+
console.log(` ${green("✓")} Data dir ${dim(config.dataDir)}`);
|
|
2479
|
+
} else {
|
|
2480
|
+
console.log(
|
|
2481
|
+
` ${yellow("!")} Data dir missing — will be created on next start`,
|
|
2482
|
+
);
|
|
2483
|
+
console.log(` ${dim(`mkdir -p "${config.dataDir}"`)}`);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// ── Vault dir ─────────────────────────────────────────────────────────
|
|
2487
|
+
if (existsSync(config.vaultDir)) {
|
|
2488
|
+
try {
|
|
2489
|
+
const probe = join(config.vaultDir, ".write-probe");
|
|
2490
|
+
writeFileSync(probe, "");
|
|
2491
|
+
unlinkSync(probe);
|
|
2492
|
+
console.log(` ${green("✓")} Vault dir ${dim(config.vaultDir)}`);
|
|
2493
|
+
} catch {
|
|
2494
|
+
console.log(` ${red("✘")} Vault dir not writable: ${config.vaultDir}`);
|
|
2495
|
+
console.log(` ${dim(`Fix: chmod u+w "${config.vaultDir}"`)}`);
|
|
2496
|
+
allOk = false;
|
|
2497
|
+
}
|
|
2498
|
+
} else {
|
|
2499
|
+
console.log(
|
|
2500
|
+
` ${yellow("!")} Vault dir missing — will be created on next start`,
|
|
2501
|
+
);
|
|
2502
|
+
console.log(` ${dim(`mkdir -p "${config.vaultDir}"`)}`);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
2506
|
+
if (existsSync(config.dbPath)) {
|
|
2507
|
+
try {
|
|
2508
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2509
|
+
const db = await initDatabase(config.dbPath);
|
|
2510
|
+
db.close();
|
|
2511
|
+
console.log(` ${green("✓")} Database ${dim(config.dbPath)}`);
|
|
2512
|
+
} catch (e) {
|
|
2513
|
+
console.log(` ${red("✘")} Database error: ${e.message}`);
|
|
2514
|
+
console.log(
|
|
2515
|
+
` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
|
|
2516
|
+
);
|
|
2517
|
+
allOk = false;
|
|
2518
|
+
}
|
|
2519
|
+
} else {
|
|
2520
|
+
console.log(
|
|
2521
|
+
` ${yellow("!")} Database missing — will be created on next start`,
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// ── Launcher (server.mjs) ─────────────────────────────────────────────
|
|
2526
|
+
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
2527
|
+
if (existsSync(launcherPath)) {
|
|
2528
|
+
const launcherContent = readFileSync(launcherPath, "utf-8");
|
|
2529
|
+
const match = launcherContent.match(/import "(.+?)"/);
|
|
2530
|
+
if (match) {
|
|
2531
|
+
const serverEntryPath = match[1];
|
|
2532
|
+
if (existsSync(serverEntryPath)) {
|
|
2533
|
+
console.log(
|
|
2534
|
+
` ${green("✓")} Launcher ${dim(`→ ${serverEntryPath}`)}`,
|
|
2535
|
+
);
|
|
2536
|
+
} else {
|
|
2537
|
+
console.log(
|
|
2538
|
+
` ${red("✘")} Launcher points to missing server: ${serverEntryPath}`,
|
|
2539
|
+
);
|
|
2540
|
+
console.log(
|
|
2541
|
+
` ${dim("Fix: run context-vault setup to reinstall")}`,
|
|
2542
|
+
);
|
|
2543
|
+
allOk = false;
|
|
2544
|
+
}
|
|
2545
|
+
} else {
|
|
2546
|
+
console.log(` ${green("✓")} Launcher exists ${dim(launcherPath)}`);
|
|
2547
|
+
}
|
|
2548
|
+
} else {
|
|
2549
|
+
console.log(` ${yellow("!")} Launcher not found at ${launcherPath}`);
|
|
2550
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2551
|
+
allOk = false;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// ── Error log ─────────────────────────────────────────────────────────
|
|
2555
|
+
const logPath = errorLogPath(config.dataDir);
|
|
2556
|
+
const logCount = errorLogCount(config.dataDir);
|
|
2557
|
+
if (logCount > 0) {
|
|
2558
|
+
console.log();
|
|
2559
|
+
console.log(
|
|
2560
|
+
` ${yellow("!")} Error log has ${logCount} entr${logCount === 1 ? "y" : "ies"}: ${dim(logPath)}`,
|
|
2561
|
+
);
|
|
2562
|
+
try {
|
|
2563
|
+
const lines = readFileSync(logPath, "utf-8")
|
|
2564
|
+
.split("\n")
|
|
2565
|
+
.filter((l) => l.trim());
|
|
2566
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
2567
|
+
console.log(` Last error: ${red(last.message)}`);
|
|
2568
|
+
console.log(
|
|
2569
|
+
` Phase: ${dim(last.phase || "unknown")} Time: ${dim(last.timestamp)}`,
|
|
2570
|
+
);
|
|
2571
|
+
} catch {}
|
|
2572
|
+
console.log(` ${dim(`To clear: rm "${logPath}"`)}`);
|
|
2573
|
+
allOk = false;
|
|
2574
|
+
} else {
|
|
2575
|
+
console.log(` ${green("✓")} No startup errors logged`);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// ── MCP tool configs ──────────────────────────────────────────────────────
|
|
2580
|
+
console.log();
|
|
2581
|
+
console.log(bold(" Tool Configurations"));
|
|
2582
|
+
const claudeConfigPath = join(HOME, ".claude.json");
|
|
2583
|
+
if (existsSync(claudeConfigPath)) {
|
|
2584
|
+
try {
|
|
2585
|
+
const claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
|
|
2586
|
+
const servers = claudeConfig?.mcpServers || {};
|
|
2587
|
+
if (servers["context-vault"]) {
|
|
2588
|
+
const srv = servers["context-vault"];
|
|
2589
|
+
const cmd = [srv.command, ...(srv.args || [])].join(" ");
|
|
2590
|
+
console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
|
|
2591
|
+
} else {
|
|
2592
|
+
console.log(` ${dim("-")} Claude Code: context-vault not configured`);
|
|
2593
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2594
|
+
}
|
|
2595
|
+
} catch {
|
|
2596
|
+
console.log(
|
|
2597
|
+
` ${yellow("!")} Claude Code: could not read ~/.claude.json`,
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
} else {
|
|
2601
|
+
console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
2605
|
+
console.log();
|
|
2606
|
+
if (allOk) {
|
|
2607
|
+
console.log(
|
|
2608
|
+
` ${green("✓ All checks passed.")} If the MCP server still fails, try:`,
|
|
2609
|
+
);
|
|
2610
|
+
console.log(
|
|
2611
|
+
` ${dim("context-vault setup")} — reconfigure tool integrations`,
|
|
2612
|
+
);
|
|
2613
|
+
} else {
|
|
2614
|
+
console.log(
|
|
2615
|
+
` ${yellow("Some issues found.")} Address the items above, then restart your AI tool.`,
|
|
2616
|
+
);
|
|
2617
|
+
console.log(
|
|
2618
|
+
` ${dim("context-vault setup")} — reconfigure and repair installation`,
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
console.log();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2263
2624
|
async function runServe() {
|
|
2264
2625
|
await import("../src/server/index.js");
|
|
2265
2626
|
}
|
|
@@ -2291,6 +2652,9 @@ async function main() {
|
|
|
2291
2652
|
case "hooks":
|
|
2292
2653
|
await runHooks();
|
|
2293
2654
|
break;
|
|
2655
|
+
case "flush":
|
|
2656
|
+
await runFlush();
|
|
2657
|
+
break;
|
|
2294
2658
|
case "recall":
|
|
2295
2659
|
await runRecall();
|
|
2296
2660
|
break;
|
|
@@ -2321,6 +2685,9 @@ async function main() {
|
|
|
2321
2685
|
case "migrate":
|
|
2322
2686
|
await runMigrate();
|
|
2323
2687
|
break;
|
|
2688
|
+
case "doctor":
|
|
2689
|
+
await runDoctor();
|
|
2690
|
+
break;
|
|
2324
2691
|
default:
|
|
2325
2692
|
console.error(red(`Unknown command: ${command}`));
|
|
2326
2693
|
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
|
+
}
|
|
@@ -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,7 @@ 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";
|
|
14
15
|
|
|
15
16
|
const toolModules = [
|
|
16
17
|
getContext,
|
|
@@ -20,6 +21,7 @@ const toolModules = [
|
|
|
20
21
|
submitFeedback,
|
|
21
22
|
ingestUrl,
|
|
22
23
|
contextStatus,
|
|
24
|
+
clearContext,
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
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.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@context-vault/core"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@context-vault/core": "^2.
|
|
58
|
+
"@context-vault/core": "^2.13.0",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
60
|
"sqlite-vec": "^0.1.0"
|
|
61
61
|
}
|
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);
|