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 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 reindex` | Rebuild search index |
51
- | `context-vault import <path>` | Import .md, .csv, .json, .txt |
52
- | `context-vault export` | Export to JSON or CSV |
53
- | `context-vault ingest <url>` | Fetch URL and save as vault entry |
54
- | `context-vault update` | Check for updates |
55
- | `context-vault uninstall` | Remove MCP configs |
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
- ${cyan("hooks remove")} Remove the hook from ~/.claude/settings.json
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.`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -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.12.0",
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.12.0",
58
+ "@context-vault/core": "^2.13.0",
59
59
  "@modelcontextprotocol/sdk": "^1.26.0",
60
60
  "sqlite-vec": "^0.1.0"
61
61
  }
@@ -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);