context-vault 2.11.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
@@ -25,6 +25,7 @@ import { join, resolve, dirname } from "node:path";
25
25
  import { homedir, platform } from "node:os";
26
26
  import { execSync, execFile, execFileSync } from "node:child_process";
27
27
  import { fileURLToPath } from "node:url";
28
+ import { APP_URL, API_URL, MARKETING_URL } from "@context-vault/core/constants";
28
29
 
29
30
  const __filename = fileURLToPath(import.meta.url);
30
31
  const __dirname = dirname(__filename);
@@ -235,10 +236,12 @@ ${bold("Commands:")}
235
236
  ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
236
237
  ${cyan("serve")} Start the MCP server (used by AI clients)
237
238
  ${cyan("hooks")} install|remove Install or remove Claude Code memory hook
239
+ ${cyan("flush")} Check vault health and confirm DB is accessible
238
240
  ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
239
241
  ${cyan("reindex")} Rebuild search index from knowledge files
240
242
  ${cyan("prune")} Remove expired entries (use --dry-run to preview)
241
243
  ${cyan("status")} Show vault diagnostics
244
+ ${cyan("doctor")} Diagnose and repair common issues
242
245
  ${cyan("update")} Check for and install updates
243
246
  ${cyan("uninstall")} Remove MCP configs and optionally data
244
247
  ${cyan("import")} <path> Import entries from file or directory
@@ -272,9 +275,39 @@ async function runSetup() {
272
275
  existingVault = cfg.vaultDir || existingVault;
273
276
  } catch {}
274
277
 
278
+ // Version check against npm registry (5s timeout, fail silently if offline)
279
+ let latestVersion = null;
280
+ try {
281
+ latestVersion = execSync("npm view context-vault version", {
282
+ encoding: "utf-8",
283
+ stdio: ["pipe", "pipe", "pipe"],
284
+ timeout: 5000,
285
+ }).trim();
286
+ } catch {}
287
+
288
+ if (latestVersion === VERSION) {
289
+ console.log(
290
+ green(` ✓ context-vault v${VERSION} is up to date`) +
291
+ dim(` (vault: ${existingVault})`),
292
+ );
293
+ console.log();
294
+ return;
295
+ }
296
+
275
297
  console.log(yellow(` Existing installation detected`));
276
298
  console.log(dim(` Vault: ${existingVault}`));
277
- console.log(dim(` Config: ${existingConfig}`));
299
+ if (latestVersion) {
300
+ console.log();
301
+ console.log(` Current: ${dim(VERSION)}`);
302
+ console.log(` Latest: ${green(latestVersion)}`);
303
+ const upgradeCmd = isNpx()
304
+ ? "npx context-vault@latest setup"
305
+ : "npm install -g context-vault";
306
+ console.log();
307
+ console.log(dim(` To upgrade: ${upgradeCmd}`));
308
+ } else {
309
+ console.log(dim(` Config: ${existingConfig}`));
310
+ }
278
311
  console.log();
279
312
  console.log(` 1) Full reconfigure`);
280
313
  console.log(` 2) Update tool configs only ${dim("(skip vault setup)")}`);
@@ -354,6 +387,7 @@ async function runSetup() {
354
387
 
355
388
  console.log();
356
389
  console.log(green(" ✓ Tool configs updated."));
390
+ console.log(dim(" Restart your AI tools to apply the changes."));
357
391
  console.log();
358
392
  return;
359
393
  }
@@ -501,7 +535,7 @@ async function runSetup() {
501
535
  console.log(
502
536
  dim(" file paths, or personal data is ever sent. Off by default."),
503
537
  );
504
- console.log(dim(" Full schema: https://contextvault.dev/telemetry"));
538
+ console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
505
539
  console.log();
506
540
 
507
541
  let telemetryEnabled = vaultConfig.telemetry === true;
@@ -541,7 +575,12 @@ async function runSetup() {
541
575
  console.log(
542
576
  `\n ${dim("[4/6]")}${bold(" Downloading embedding model...")}`,
543
577
  );
544
- console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
578
+ console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)"));
579
+ console.log(
580
+ dim(
581
+ ` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`,
582
+ ),
583
+ );
545
584
  {
546
585
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
547
586
  let frame = 0;
@@ -731,7 +770,9 @@ async function runSetup() {
731
770
  const boxLines = [
732
771
  ` ✓ Setup complete — ${passed}/${checks.length} checks passed (${elapsed}s)`,
733
772
  ``,
734
- ` ${bold("AI Tools")} open ${toolName} and try:`,
773
+ ` ${bold("Next:")} restart ${toolName} to activate the vault`,
774
+ ``,
775
+ ` ${bold("AI Tools")} — once active, try:`,
735
776
  ` "Search my vault for getting started"`,
736
777
  ` "Save an insight about [topic]"`,
737
778
  ` "Show my vault status"`,
@@ -979,7 +1020,7 @@ This is an example entry showing the decision format. Feel free to delete it.
979
1020
 
980
1021
  async function runConnect() {
981
1022
  const apiKey = getFlag("--key");
982
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1023
+ const hostedUrl = getFlag("--url") || API_URL;
983
1024
 
984
1025
  if (!apiKey) {
985
1026
  console.log(`\n ${bold("context-vault connect")}\n`);
@@ -988,9 +1029,7 @@ async function runConnect() {
988
1029
  console.log(` context-vault connect --key cv_...\n`);
989
1030
  console.log(` Options:`);
990
1031
  console.log(` --key <key> API key (required)`);
991
- console.log(
992
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
993
- );
1032
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
994
1033
  console.log();
995
1034
  return;
996
1035
  }
@@ -1232,9 +1271,7 @@ async function runSwitch() {
1232
1271
  );
1233
1272
  console.log(` Options:`);
1234
1273
  console.log(` --key <key> API key for hosted mode (cv_...)`);
1235
- console.log(
1236
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)\n`,
1237
- );
1274
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})\n`);
1238
1275
  return;
1239
1276
  }
1240
1277
 
@@ -1291,10 +1328,7 @@ async function runSwitch() {
1291
1328
  console.log(dim(` Server: node ${launcherPath}`));
1292
1329
  console.log();
1293
1330
  } else {
1294
- const hostedUrl =
1295
- getFlag("--url") ||
1296
- vaultConfig.hostedUrl ||
1297
- "https://api.context-vault.com";
1331
+ const hostedUrl = getFlag("--url") || vaultConfig.hostedUrl || API_URL;
1298
1332
  const apiKey = getFlag("--key") || vaultConfig.apiKey;
1299
1333
 
1300
1334
  if (!apiKey) {
@@ -1452,6 +1486,8 @@ async function runStatus() {
1452
1486
  const { resolveConfig } = await import("@context-vault/core/core/config");
1453
1487
  const { initDatabase } = await import("@context-vault/core/index/db");
1454
1488
  const { gatherVaultStatus } = await import("@context-vault/core/core/status");
1489
+ const { errorLogPath, errorLogCount } =
1490
+ await import("@context-vault/core/core/error-log");
1455
1491
 
1456
1492
  const config = resolveConfig();
1457
1493
 
@@ -1534,6 +1570,18 @@ async function runStatus() {
1534
1570
  console.log(yellow(" Stale paths detected in DB."));
1535
1571
  console.log(` Run ${cyan("context-vault reindex")} to update.`);
1536
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
+ }
1537
1585
  console.log();
1538
1586
  }
1539
1587
 
@@ -1674,15 +1722,13 @@ async function runMigrate() {
1674
1722
  ` context-vault migrate --to-local Download hosted vault to local files`,
1675
1723
  );
1676
1724
  console.log(`\n Options:`);
1677
- console.log(
1678
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
1679
- );
1725
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
1680
1726
  console.log(` --key <key> API key (cv_...)`);
1681
1727
  console.log();
1682
1728
  return;
1683
1729
  }
1684
1730
 
1685
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1731
+ const hostedUrl = getFlag("--url") || API_URL;
1686
1732
  const apiKey = getFlag("--key");
1687
1733
 
1688
1734
  if (!apiKey) {
@@ -2099,6 +2145,37 @@ async function runRecall() {
2099
2145
  }
2100
2146
  }
2101
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
+
2102
2179
  /** Returns the path to Claude Code's global settings.json */
2103
2180
  function claudeSettingsPath() {
2104
2181
  return join(HOME, ".claude", "settings.json");
@@ -2146,6 +2223,76 @@ function installClaudeHook() {
2146
2223
  return true;
2147
2224
  }
2148
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
+
2149
2296
  /**
2150
2297
  * Removes the context-vault recall hook from ~/.claude/settings.json.
2151
2298
  * Returns true if removed, false if not found.
@@ -2203,6 +2350,44 @@ async function runHooks() {
2203
2350
  process.exit(1);
2204
2351
  }
2205
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
+ }
2206
2391
  } else if (sub === "remove") {
2207
2392
  try {
2208
2393
  const removed = removeClaudeHook();
@@ -2215,6 +2400,19 @@ async function runHooks() {
2215
2400
  console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
2216
2401
  process.exit(1);
2217
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
+ }
2218
2416
  } else {
2219
2417
  console.log(`
2220
2418
  ${bold("context-vault hooks")} <install|remove>
@@ -2225,11 +2423,204 @@ async function runHooks() {
2225
2423
 
2226
2424
  ${bold("Commands:")}
2227
2425
  ${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
2228
- ${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
2229
2428
  `);
2230
2429
  }
2231
2430
  }
2232
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
+
2233
2624
  async function runServe() {
2234
2625
  await import("../src/server/index.js");
2235
2626
  }
@@ -2261,6 +2652,9 @@ async function main() {
2261
2652
  case "hooks":
2262
2653
  await runHooks();
2263
2654
  break;
2655
+ case "flush":
2656
+ await runFlush();
2657
+ break;
2264
2658
  case "recall":
2265
2659
  await runRecall();
2266
2660
  break;
@@ -2291,6 +2685,9 @@ async function main() {
2291
2685
  case "migrate":
2292
2686
  await runMigrate();
2293
2687
  break;
2688
+ case "doctor":
2689
+ await runDoctor();
2690
+ break;
2294
2691
  default:
2295
2692
  console.error(red(`Unknown command: ${command}`));
2296
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.11.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",
@@ -1,3 +1,9 @@
1
+ export const APP_URL = "https://app.context-vault.com";
2
+ export const API_URL = "https://api.context-vault.com";
3
+ export const MARKETING_URL = "https://contextvault.dev";
4
+ export const GITHUB_ISSUES_URL =
5
+ "https://github.com/fellanH/context-vault/issues";
6
+
1
7
  export const MAX_BODY_LENGTH = 100 * 1024; // 100KB
2
8
  export const MAX_TITLE_LENGTH = 500;
3
9
  export const MAX_KIND_LENGTH = 64;
@@ -1,8 +1,10 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "../constants.js";
3
4
 
4
- const TELEMETRY_ENDPOINT = "https://api.context-vault.com/telemetry";
5
+ const TELEMETRY_ENDPOINT = `${API_URL}/telemetry`;
5
6
  const NOTICE_MARKER = ".telemetry-notice-shown";
7
+ const FEEDBACK_PROMPT_MARKER = ".feedback-prompt-shown";
6
8
 
7
9
  export function isTelemetryEnabled(config) {
8
10
  const envVal = process.env.CONTEXT_VAULT_TELEMETRY;
@@ -56,7 +58,31 @@ export function maybeShowTelemetryNotice(dataDir) {
56
58
  "[context-vault] Reports contain only: event type, error code, tool name, version, node version, platform, arch, timestamp.",
57
59
  "[context-vault] No vault content, file paths, or personal data is ever sent.",
58
60
  '[context-vault] Opt in: set "telemetry": true in ~/.context-mcp/config.json or set CONTEXT_VAULT_TELEMETRY=1.',
59
- "[context-vault] Full payload schema: https://contextvault.dev/telemetry",
61
+ `[context-vault] Full payload schema: ${MARKETING_URL}/telemetry`,
62
+ ];
63
+ for (const line of lines) {
64
+ process.stderr.write(line + "\n");
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Print a one-time feedback prompt after the user's first successful save.
70
+ * Uses a marker file in dataDir to ensure it's only shown once.
71
+ * Never throws, never blocks.
72
+ */
73
+ export function maybeShowFeedbackPrompt(dataDir) {
74
+ try {
75
+ const markerPath = join(dataDir, FEEDBACK_PROMPT_MARKER);
76
+ if (existsSync(markerPath)) return;
77
+ writeFileSync(markerPath, new Date().toISOString() + "\n");
78
+ } catch {
79
+ return;
80
+ }
81
+
82
+ const lines = [
83
+ "[context-vault] First entry saved — nice work!",
84
+ "[context-vault] Got feedback, a bug, or a feature request?",
85
+ `[context-vault] Open an issue: ${GITHUB_ISSUES_URL}`,
60
86
  ];
61
87
  for (const line of lines) {
62
88
  process.stderr.write(line + "\n");
@@ -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
  }
@@ -4,6 +4,7 @@ import { indexEntry } from "../../index/index.js";
4
4
  import { categoryFor } from "../../core/categories.js";
5
5
  import { normalizeKind } from "../../core/files.js";
6
6
  import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
7
+ import { maybeShowFeedbackPrompt } from "../../core/telemetry.js";
7
8
  import {
8
9
  MAX_BODY_LENGTH,
9
10
  MAX_TITLE_LENGTH,
@@ -398,6 +399,11 @@ export async function handler(
398
399
  supersedes,
399
400
  userId,
400
401
  });
402
+
403
+ if (ctx.config?.dataDir) {
404
+ maybeShowFeedbackPrompt(ctx.config.dataDir);
405
+ }
406
+
401
407
  const relPath = entry.filePath
402
408
  ? entry.filePath.replace(config.vaultDir + "/", "")
403
409
  : entry.filePath;
@@ -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.11.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.11.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);