context-vault 2.16.0 → 2.17.1

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.
Files changed (27) hide show
  1. package/bin/cli.js +513 -63
  2. package/node_modules/@context-vault/core/package.json +2 -2
  3. package/node_modules/@context-vault/core/src/capture/file-ops.js +2 -0
  4. package/node_modules/@context-vault/core/src/capture/index.js +14 -0
  5. package/node_modules/@context-vault/core/src/constants.js +7 -2
  6. package/node_modules/@context-vault/core/src/core/categories.js +1 -0
  7. package/node_modules/@context-vault/core/src/core/config.js +8 -1
  8. package/node_modules/@context-vault/core/src/core/files.js +6 -29
  9. package/node_modules/@context-vault/core/src/core/frontmatter.js +1 -0
  10. package/node_modules/@context-vault/core/src/core/linking.js +161 -0
  11. package/node_modules/@context-vault/core/src/core/migrate-dirs.js +196 -0
  12. package/node_modules/@context-vault/core/src/core/status.js +28 -2
  13. package/node_modules/@context-vault/core/src/core/temporal.js +146 -0
  14. package/node_modules/@context-vault/core/src/index/db.js +178 -8
  15. package/node_modules/@context-vault/core/src/index/index.js +113 -48
  16. package/node_modules/@context-vault/core/src/index.js +5 -0
  17. package/node_modules/@context-vault/core/src/retrieve/index.js +16 -136
  18. package/node_modules/@context-vault/core/src/server/tools/context-status.js +7 -0
  19. package/node_modules/@context-vault/core/src/server/tools/create-snapshot.js +37 -68
  20. package/node_modules/@context-vault/core/src/server/tools/get-context.js +120 -19
  21. package/node_modules/@context-vault/core/src/server/tools/save-context.js +29 -6
  22. package/node_modules/@context-vault/core/src/server/tools.js +0 -2
  23. package/package.json +3 -3
  24. package/src/hooks/post-tool-call.mjs +1 -1
  25. package/src/hooks/session-end.mjs +1 -1
  26. package/src/server/index.js +4 -2
  27. package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +0 -55
package/bin/cli.js CHANGED
@@ -282,7 +282,7 @@ function printDetectionResults(results) {
282
282
  }
283
283
  }
284
284
 
285
- function showHelp() {
285
+ function showHelp(showAll = false) {
286
286
  console.log(`
287
287
  ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}
288
288
  ${dim("Persistent memory for AI agents")}
@@ -293,35 +293,48 @@ ${bold("Usage:")}
293
293
  ${dim("No command → runs setup (first time) or shows status (existing vault)")}
294
294
 
295
295
  ${bold("Commands:")}
296
- ${cyan("setup")} Interactive MCP server installer
297
- ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
298
- ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
299
- ${cyan("serve")} Start the MCP server (used by AI clients)
300
- ${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
301
- ${cyan("claude")} install|uninstall Alias for hooks install|uninstall
302
- ${cyan("skills")} install Install bundled Claude Code skills
303
- ${cyan("health")} Quick health check — vault, DB, entry count
304
- ${cyan("restart")} Stop running MCP server processes (client auto-restarts)
305
- ${cyan("flush")} Check vault health and confirm DB is accessible
306
- ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
307
- ${cyan("session-capture")} Save a session summary entry (reads JSON from stdin)
308
- ${cyan("save")} Save an entry to the vault from CLI
309
- ${cyan("search")} Search vault entries from CLI
310
- ${cyan("reindex")} Rebuild search index from knowledge files
311
- ${cyan("prune")} Remove expired entries (use --dry-run to preview)
312
- ${cyan("status")} Show vault diagnostics
313
- ${cyan("doctor")} Diagnose and repair common issues
314
- ${cyan("update")} Check for and install updates
315
- ${cyan("uninstall")} Remove MCP configs and optionally data
316
- ${cyan("import")} <path> Import entries from file or directory
317
- ${cyan("export")} Export vault to JSON or CSV
318
- ${cyan("ingest")} <url> Fetch URL and save as vault entry
319
- ${cyan("ingest-project")} <path> Scan project directory and register as project entity
320
- ${cyan("migrate")} Migrate vault between local and hosted
321
- ${cyan("consolidate")} Find hot tags and cold entries for maintenance
322
-
323
- ${bold("Options:")}
296
+ ${cyan("setup")} Interactive MCP server installer
297
+ ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
298
+ ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
299
+ ${cyan("serve")} Start the MCP server (used by AI clients)
300
+ ${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
301
+ ${cyan("claude")} install|uninstall Alias for hooks install|uninstall
302
+ ${cyan("skills")} install Install bundled Claude Code skills
303
+ ${cyan("health")} Quick health check — vault, DB, entry count
304
+ ${cyan("status")} Show vault diagnostics
305
+ ${cyan("doctor")} Diagnose and repair common issues
306
+ ${cyan("restart")} Stop running MCP server processes (client auto-restarts)
307
+ ${cyan("search")} Search vault entries from CLI
308
+ ${cyan("save")} Save an entry to the vault from CLI
309
+ ${cyan("import")} <path> Import entries from file or directory
310
+ ${cyan("export")} Export vault to JSON or CSV
311
+ ${cyan("ingest")} <url> Fetch URL and save as vault entry
312
+ ${cyan("ingest-project")} <path> Scan project directory and register as project entity
313
+ ${cyan("reindex")} Rebuild search index from knowledge files
314
+ ${cyan("migrate-dirs")} [--dry-run] Rename plural vault dirs to singular (post-2.18.0)
315
+ ${cyan("prune")} Remove expired entries (use --dry-run to preview)
316
+ ${cyan("update")} Check for and install updates
317
+ ${cyan("uninstall")} Remove MCP configs and optionally data
318
+ `);
319
+
320
+ if (showAll) {
321
+ console.log(`${bold("Plumbing")} ${dim("(internal hook implementations and maintenance utilities):")}
322
+ ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
323
+ ${cyan("session-capture")} Save a session summary entry (reads JSON from stdin)
324
+ ${cyan("session-end")} Run session-end hook (parse transcript + capture)
325
+ ${cyan("post-tool-call")} Run post-tool-call hook (log tool usage)
326
+ ${cyan("flush")} Check vault health and confirm DB is accessible
327
+ ${cyan("consolidate")} Find hot tags and cold entries for maintenance
328
+ ${cyan("migrate")} Migrate vault between local and hosted
329
+ `);
330
+ } else {
331
+ console.log(` ${dim("Run")} ${dim("context-vault --help --all")} ${dim("to show internal plumbing commands.")}
332
+ `);
333
+ }
334
+
335
+ console.log(`${bold("Options:")}
324
336
  --help Show this help
337
+ --help --all Show all commands including internal plumbing
325
338
  --version Show version
326
339
  --vault-dir <path> Set vault directory (setup/serve)
327
340
  --yes Non-interactive mode (accept all defaults)
@@ -1694,6 +1707,80 @@ async function runReindex() {
1694
1707
  console.log(` ${dim("·")} ${stats.unchanged} unchanged`);
1695
1708
  }
1696
1709
 
1710
+ async function runMigrateDirs() {
1711
+ const dryRun = flags.has("--dry-run");
1712
+
1713
+ // Vault dir: positional arg (skip --flags), or fall back to configured vault
1714
+ const positional = args.slice(1).find((a) => !a.startsWith("--"));
1715
+ let vaultDir = positional;
1716
+
1717
+ if (!vaultDir) {
1718
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1719
+ const config = resolveConfig();
1720
+ if (!config.vaultDirExists) {
1721
+ console.error(red(`Vault directory not found: ${config.vaultDir}`));
1722
+ console.error("Run " + cyan("context-vault setup") + " to configure.");
1723
+ process.exit(1);
1724
+ }
1725
+ vaultDir = config.vaultDir;
1726
+ }
1727
+
1728
+ if (!existsSync(vaultDir) || !statSync(vaultDir).isDirectory()) {
1729
+ console.error(red(`Error: ${vaultDir} is not a directory`));
1730
+ process.exit(1);
1731
+ }
1732
+
1733
+ const { planMigration, executeMigration } =
1734
+ await import("@context-vault/core/core/migrate-dirs");
1735
+
1736
+ const ops = planMigration(vaultDir);
1737
+
1738
+ if (ops.length === 0) {
1739
+ console.log(green("✓ No plural directories found — vault is up to date."));
1740
+ return;
1741
+ }
1742
+
1743
+ if (dryRun) {
1744
+ console.log(dim("Dry run — no files will be moved.\n"));
1745
+ }
1746
+
1747
+ for (const op of ops) {
1748
+ const fileLabel = `${op.fileCount} ${op.fileCount === 1 ? "file" : "files"}`;
1749
+ const actionLabel = op.action === "rename" ? "RENAME" : "MERGE";
1750
+ const suffix = dryRun ? dim(" [dry-run]") : "";
1751
+ console.log(
1752
+ ` ${cyan(actionLabel)}: ${op.pluralName}/ → ${op.singularName}/ (${fileLabel})${suffix}`,
1753
+ );
1754
+ }
1755
+
1756
+ if (dryRun) {
1757
+ console.log();
1758
+ console.log(
1759
+ dim(
1760
+ ` ${ops.length} ${ops.length === 1 ? "directory" : "directories"} would be renamed/merged.`,
1761
+ ),
1762
+ );
1763
+ console.log(dim(" Remove --dry-run to apply."));
1764
+ return;
1765
+ }
1766
+
1767
+ const { renamed, merged, errors } = executeMigration(ops);
1768
+
1769
+ console.log();
1770
+ if (renamed > 0) console.log(green(`✓ Renamed: ${renamed}`));
1771
+ if (merged > 0) console.log(green(`✓ Merged: ${merged}`));
1772
+ if (errors.length > 0) {
1773
+ for (const e of errors) console.log(red(` ✗ ${e}`));
1774
+ }
1775
+
1776
+ if (renamed + merged > 0) {
1777
+ console.log();
1778
+ console.log(
1779
+ dim("Run `context-vault reindex` to rebuild the search index."),
1780
+ );
1781
+ }
1782
+ }
1783
+
1697
1784
  async function runPrune() {
1698
1785
  const dryRun = flags.has("--dry-run");
1699
1786
 
@@ -1783,7 +1870,18 @@ async function runStatus() {
1783
1870
  } catch {}
1784
1871
  }
1785
1872
 
1786
- const db = await initDatabase(config.dbPath);
1873
+ let db;
1874
+ try {
1875
+ db = await initDatabase(config.dbPath);
1876
+ } catch (e) {
1877
+ console.log();
1878
+ console.log(` ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}`);
1879
+ console.log();
1880
+ console.log(` ${red("✘")} Database not accessible: ${e.message}`);
1881
+ console.log(dim(` Run ${cyan("context-vault doctor")} for diagnostics`));
1882
+ console.log();
1883
+ process.exit(1);
1884
+ }
1787
1885
 
1788
1886
  const status = gatherVaultStatus({ db, config });
1789
1887
 
@@ -1814,7 +1912,7 @@ async function runStatus() {
1814
1912
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
1815
1913
  const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
1816
1914
  const countStr = String(c).padStart(4);
1817
- console.log(` ${countStr} ${kind}s ${dim(bar)}`);
1915
+ console.log(` ${dim(bar)} ${countStr} ${kind}s`);
1818
1916
  }
1819
1917
  } else {
1820
1918
  console.log(`\n ${dim("(empty — no entries indexed)")}`);
@@ -1956,6 +2054,25 @@ async function runUninstall() {
1956
2054
  }
1957
2055
  }
1958
2056
 
2057
+ // Remove Claude Code hooks
2058
+ const recallRemoved = removeClaudeHook();
2059
+ const captureRemoved = removeSessionCaptureHook();
2060
+ const flushRemoved = removeSessionEndHook();
2061
+ const autoCaptureRemoved = removePostToolCallHook();
2062
+ if (recallRemoved || captureRemoved || flushRemoved || autoCaptureRemoved) {
2063
+ console.log(` ${green("+")} Removed Claude Code hooks`);
2064
+ } else {
2065
+ console.log(` ${dim("-")} No Claude Code hooks to remove`);
2066
+ }
2067
+
2068
+ // Remove installed skills
2069
+ const skillsDir = join(HOME, ".claude", "skills", "compile-context");
2070
+ if (existsSync(skillsDir)) {
2071
+ const { rmSync } = await import("node:fs");
2072
+ rmSync(skillsDir, { recursive: true, force: true });
2073
+ console.log(` ${green("+")} Removed installed skills`);
2074
+ }
2075
+
1959
2076
  // Optionally remove data directory
1960
2077
  const dataDir = join(HOME, ".context-mcp");
1961
2078
  if (existsSync(dataDir)) {
@@ -2745,6 +2862,16 @@ async function runSessionCapture() {
2745
2862
  }
2746
2863
  }
2747
2864
 
2865
+ async function runSessionEnd() {
2866
+ const { main } = await import("../src/hooks/session-end.mjs");
2867
+ await main();
2868
+ }
2869
+
2870
+ async function runPostToolCall() {
2871
+ const { main } = await import("../src/hooks/post-tool-call.mjs");
2872
+ await main();
2873
+ }
2874
+
2748
2875
  async function runSave() {
2749
2876
  const kind = getFlag("--kind");
2750
2877
  const title = getFlag("--title");
@@ -2839,6 +2966,7 @@ async function runSearch() {
2839
2966
  const sort = getFlag("--sort") || "relevance";
2840
2967
  const format = getFlag("--format") || "plain";
2841
2968
  const showFull = flags.has("--full");
2969
+ const scopeArg = getFlag("--scope"); // "hot" | "events" | "all"
2842
2970
 
2843
2971
  const valuedFlags = new Set([
2844
2972
  "--kind",
@@ -2846,6 +2974,7 @@ async function runSearch() {
2846
2974
  "--limit",
2847
2975
  "--sort",
2848
2976
  "--format",
2977
+ "--scope",
2849
2978
  ]);
2850
2979
 
2851
2980
  const queryParts = [];
@@ -2885,8 +3014,18 @@ async function runSearch() {
2885
3014
 
2886
3015
  let results;
2887
3016
 
3017
+ // Resolve scope → category/exclude filter
3018
+ const validScopes = new Set(["hot", "events", "all"]);
3019
+ const resolvedScope = validScopes.has(scopeArg) ? scopeArg : "hot";
3020
+ const scopeCategoryFilter = resolvedScope === "events" ? "event" : null;
3021
+ const scopeExcludeEvents = resolvedScope === "hot";
3022
+
2888
3023
  if (query) {
2889
- results = await hybridSearch(ctx, query, { limit: limit * 2 });
3024
+ results = await hybridSearch(ctx, query, {
3025
+ limit: limit * 2,
3026
+ categoryFilter: scopeCategoryFilter,
3027
+ excludeEvents: scopeExcludeEvents,
3028
+ });
2890
3029
 
2891
3030
  if (kind) {
2892
3031
  results = results.filter((r) => r.kind === kind);
@@ -2899,6 +3038,10 @@ async function runSearch() {
2899
3038
  sql += " AND kind = ?";
2900
3039
  params.push(kind);
2901
3040
  }
3041
+ if (scopeCategoryFilter) {
3042
+ sql += " AND category = ?";
3043
+ params.push(scopeCategoryFilter);
3044
+ }
2902
3045
  sql += " ORDER BY COALESCE(updated_at, created_at) DESC LIMIT ?";
2903
3046
  params.push(limit);
2904
3047
  results = db.prepare(sql).all(...params);
@@ -3160,14 +3303,6 @@ function removeClaudeHook() {
3160
3303
  return true;
3161
3304
  }
3162
3305
 
3163
- function sessionEndHookPath() {
3164
- return resolve(ROOT, "src", "hooks", "session-end.mjs");
3165
- }
3166
-
3167
- function postToolCallHookPath() {
3168
- return resolve(ROOT, "src", "hooks", "post-tool-call.mjs");
3169
- }
3170
-
3171
3306
  /**
3172
3307
  * Writes a SessionEnd hook entry for session capture to ~/.claude/settings.json.
3173
3308
  * Returns true if installed, false if already present.
@@ -3190,17 +3325,29 @@ function installSessionCaptureHook() {
3190
3325
  if (!settings.hooks) settings.hooks = {};
3191
3326
  if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
3192
3327
 
3328
+ const newCommand = "context-vault session-end";
3329
+
3330
+ // Check if already installed with new CLI-based command
3193
3331
  const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
3194
- h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3332
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3195
3333
  );
3196
3334
  if (alreadyInstalled) return false;
3197
3335
 
3198
- const hookScript = sessionEndHookPath();
3336
+ // Migrate: remove stale absolute-path hooks (node <path>/session-end.mjs)
3337
+ const hadStale = settings.hooks.SessionEnd.some((h) =>
3338
+ h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3339
+ );
3340
+ if (hadStale) {
3341
+ settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
3342
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3343
+ );
3344
+ }
3345
+
3199
3346
  settings.hooks.SessionEnd.push({
3200
3347
  hooks: [
3201
3348
  {
3202
3349
  type: "command",
3203
- command: `node ${hookScript}`,
3350
+ command: newCommand,
3204
3351
  timeout: 30,
3205
3352
  },
3206
3353
  ],
@@ -3230,7 +3377,12 @@ function removeSessionCaptureHook() {
3230
3377
 
3231
3378
  const before = settings.hooks.SessionEnd.length;
3232
3379
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
3233
- (h) => !h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3380
+ (h) =>
3381
+ !h.hooks?.some(
3382
+ (hh) =>
3383
+ hh.command?.includes("session-end.mjs") ||
3384
+ hh.command?.includes("context-vault session-end"),
3385
+ ),
3234
3386
  );
3235
3387
 
3236
3388
  if (settings.hooks.SessionEnd.length === before) return false;
@@ -3261,17 +3413,29 @@ function installPostToolCallHook() {
3261
3413
  if (!settings.hooks) settings.hooks = {};
3262
3414
  if (!settings.hooks.PostToolCall) settings.hooks.PostToolCall = [];
3263
3415
 
3416
+ const newCommand = "context-vault post-tool-call";
3417
+
3418
+ // Check if already installed with new CLI-based command
3264
3419
  const alreadyInstalled = settings.hooks.PostToolCall.some((h) =>
3265
- h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3420
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3266
3421
  );
3267
3422
  if (alreadyInstalled) return false;
3268
3423
 
3269
- const hookScript = postToolCallHookPath();
3424
+ // Migrate: remove stale absolute-path hooks (node <path>/post-tool-call.mjs)
3425
+ const hadStale = settings.hooks.PostToolCall.some((h) =>
3426
+ h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3427
+ );
3428
+ if (hadStale) {
3429
+ settings.hooks.PostToolCall = settings.hooks.PostToolCall.filter(
3430
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3431
+ );
3432
+ }
3433
+
3270
3434
  settings.hooks.PostToolCall.push({
3271
3435
  hooks: [
3272
3436
  {
3273
3437
  type: "command",
3274
- command: `node ${hookScript}`,
3438
+ command: newCommand,
3275
3439
  timeout: 5,
3276
3440
  },
3277
3441
  ],
@@ -3301,7 +3465,12 @@ function removePostToolCallHook() {
3301
3465
 
3302
3466
  const before = settings.hooks.PostToolCall.length;
3303
3467
  settings.hooks.PostToolCall = settings.hooks.PostToolCall.filter(
3304
- (h) => !h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3468
+ (h) =>
3469
+ !h.hooks?.some(
3470
+ (hh) =>
3471
+ hh.command?.includes("post-tool-call.mjs") ||
3472
+ hh.command?.includes("context-vault post-tool-call"),
3473
+ ),
3305
3474
  );
3306
3475
 
3307
3476
  if (settings.hooks.PostToolCall.length === before) return false;
@@ -3667,16 +3836,20 @@ async function runDoctor() {
3667
3836
  }
3668
3837
 
3669
3838
  // ── Database ──────────────────────────────────────────────────────────
3839
+ let db;
3670
3840
  if (existsSync(config.dbPath)) {
3671
3841
  try {
3672
3842
  const { initDatabase } = await import("@context-vault/core/index/db");
3673
- const db = await initDatabase(config.dbPath);
3674
- db.close();
3675
- console.log(` ${green("")} Database ${dim(config.dbPath)}`);
3843
+ db = await initDatabase(config.dbPath);
3844
+ const schemaRow = db.prepare("PRAGMA user_version").get();
3845
+ const schemaVersion = schemaRow?.user_version ?? "unknown";
3846
+ console.log(
3847
+ ` ${green("✓")} Database ${dim(`${config.dbPath} (schema v${schemaVersion})`)}`,
3848
+ );
3676
3849
  } catch (e) {
3677
3850
  console.log(` ${red("✘")} Database error: ${e.message}`);
3678
3851
  console.log(
3679
- ` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
3852
+ ` ${dim(`Fix: rm "${config.dbPath}" and restart (will rebuild from vault files)`)}`,
3680
3853
  );
3681
3854
  allOk = false;
3682
3855
  }
@@ -3686,6 +3859,101 @@ async function runDoctor() {
3686
3859
  );
3687
3860
  }
3688
3861
 
3862
+ // ── Embedding model ──────────────────────────────────────────────────
3863
+ try {
3864
+ const { embed } = await import("@context-vault/core/index/embed");
3865
+ const vec = await embed("doctor check");
3866
+ if (vec && vec.length > 0) {
3867
+ console.log(
3868
+ ` ${green("✓")} Embedding model ${dim(`(${vec.length} dimensions)`)}`,
3869
+ );
3870
+ } else {
3871
+ console.log(
3872
+ ` ${yellow("!")} Embedding model unavailable — semantic search disabled (FTS-only)`,
3873
+ );
3874
+ console.log(
3875
+ ` ${dim("Fix: run context-vault setup to download the model")}`,
3876
+ );
3877
+ }
3878
+ } catch {
3879
+ console.log(
3880
+ ` ${yellow("!")} Embedding model unavailable — semantic search disabled (FTS-only)`,
3881
+ );
3882
+ console.log(
3883
+ ` ${dim("Fix: run context-vault setup to download the model")}`,
3884
+ );
3885
+ }
3886
+
3887
+ // ── DB/filesystem consistency ─────────────────────────────────────────
3888
+ if (db && existsSync(config.vaultDir)) {
3889
+ try {
3890
+ const totalRow = db.prepare("SELECT COUNT(*) as c FROM vault").get();
3891
+ const total = totalRow?.c ?? 0;
3892
+ if (total > 0) {
3893
+ const sampleRows = db
3894
+ .prepare("SELECT file_path FROM vault LIMIT 50")
3895
+ .all();
3896
+ let staleCount = 0;
3897
+ for (const row of sampleRows) {
3898
+ if (row.file_path && !existsSync(row.file_path)) {
3899
+ staleCount++;
3900
+ }
3901
+ }
3902
+ if (staleCount > 0) {
3903
+ const pct = Math.round((staleCount / sampleRows.length) * 100);
3904
+ console.log(
3905
+ ` ${yellow("!")} ${staleCount}/${sampleRows.length} sampled DB entries point to missing files (${pct}%)`,
3906
+ );
3907
+ console.log(
3908
+ ` ${dim("Fix: run context-vault reindex to rebuild from vault files")}`,
3909
+ );
3910
+ allOk = false;
3911
+ } else {
3912
+ console.log(
3913
+ ` ${green("✓")} DB/filesystem consistency ${dim(`(${total} entries, sample OK)`)}`,
3914
+ );
3915
+ }
3916
+ }
3917
+ } catch {
3918
+ // non-critical — skip silently
3919
+ }
3920
+ }
3921
+
3922
+ // ── Auto-captured feedback entries ─────────────────────────────────────
3923
+ if (db) {
3924
+ try {
3925
+ const feedbackRow = db
3926
+ .prepare(
3927
+ `SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%'`,
3928
+ )
3929
+ .get();
3930
+ const feedbackCount = feedbackRow?.c ?? 0;
3931
+ if (feedbackCount > 0) {
3932
+ const recentRows = db
3933
+ .prepare(
3934
+ `SELECT title, created_at FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%' ORDER BY created_at DESC LIMIT 3`,
3935
+ )
3936
+ .all();
3937
+ console.log(
3938
+ ` ${yellow("!")} ${feedbackCount} auto-captured error${feedbackCount === 1 ? "" : "s"} in vault`,
3939
+ );
3940
+ for (const row of recentRows) {
3941
+ console.log(` ${dim(`${row.created_at} — ${row.title}`)}`);
3942
+ }
3943
+ console.log(
3944
+ ` ${dim("Review: context-vault search --kind feedback --tag auto-captured")}`,
3945
+ );
3946
+ }
3947
+ } catch {
3948
+ // non-critical — skip silently
3949
+ }
3950
+ }
3951
+
3952
+ // Close DB if opened
3953
+ try {
3954
+ db?.close();
3955
+ } catch {}
3956
+
3689
3957
  // ── Launcher (server.mjs) ─────────────────────────────────────────────
3690
3958
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
3691
3959
  if (existsSync(launcherPath)) {
@@ -3743,6 +4011,9 @@ async function runDoctor() {
3743
4011
  // ── MCP tool configs ──────────────────────────────────────────────────────
3744
4012
  console.log();
3745
4013
  console.log(bold(" Tool Configurations"));
4014
+ let anyToolConfigured = false;
4015
+
4016
+ // Check Claude Code
3746
4017
  const claudeConfigPath = join(HOME, ".claude.json");
3747
4018
  if (existsSync(claudeConfigPath)) {
3748
4019
  try {
@@ -3752,9 +4023,9 @@ async function runDoctor() {
3752
4023
  const srv = servers["context-vault"];
3753
4024
  const cmd = [srv.command, ...(srv.args || [])].join(" ");
3754
4025
  console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
4026
+ anyToolConfigured = true;
3755
4027
  } else {
3756
- console.log(` ${dim("-")} Claude Code: context-vault not configured`);
3757
- console.log(` ${dim("Fix: run context-vault setup")}`);
4028
+ console.log(` ${dim("-")} Claude Code: not configured`);
3758
4029
  }
3759
4030
  } catch {
3760
4031
  console.log(
@@ -3765,21 +4036,191 @@ async function runDoctor() {
3765
4036
  console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
3766
4037
  }
3767
4038
 
4039
+ // Check all JSON-configured tools
4040
+ for (const tool of TOOLS.filter((t) => t.configType === "json")) {
4041
+ const cfgPath = tool.configPath;
4042
+ if (!cfgPath || !existsSync(cfgPath)) {
4043
+ continue; // tool not installed — skip silently
4044
+ }
4045
+ try {
4046
+ const toolConfig = JSON.parse(readFileSync(cfgPath, "utf-8"));
4047
+ const servers = toolConfig?.[tool.configKey] || {};
4048
+ if (servers["context-vault"]) {
4049
+ const srv = servers["context-vault"];
4050
+ const cmd = [srv.command, ...(srv.args || [])].join(" ");
4051
+ console.log(` ${green("+")} ${tool.name}: ${dim(cmd)}`);
4052
+ anyToolConfigured = true;
4053
+ } else if (servers["context-mcp"]) {
4054
+ console.log(
4055
+ ` ${yellow("!")} ${tool.name}: using old name "context-mcp"`,
4056
+ );
4057
+ console.log(` ${dim("Fix: run context-vault setup to update")}`);
4058
+ anyToolConfigured = true;
4059
+ }
4060
+ } catch {
4061
+ // config exists but unreadable — skip
4062
+ }
4063
+ }
4064
+
4065
+ // Check Codex
4066
+ try {
4067
+ const codexCheck = execSync("codex mcp list 2>/dev/null", {
4068
+ encoding: "utf-8",
4069
+ stdio: ["pipe", "pipe", "pipe"],
4070
+ timeout: 5000,
4071
+ });
4072
+ if (codexCheck.includes("context-vault")) {
4073
+ console.log(` ${green("+")} Codex: ${dim("configured")}`);
4074
+ anyToolConfigured = true;
4075
+ }
4076
+ } catch {
4077
+ // codex not installed or not configured — skip
4078
+ }
4079
+
4080
+ if (!anyToolConfigured) {
4081
+ console.log(` ${yellow("!")} No AI tools have context-vault configured`);
4082
+ console.log(` ${dim("Fix: run context-vault setup")}`);
4083
+ allOk = false;
4084
+ }
4085
+
4086
+ // ── Claude Code hooks ──────────────────────────────────────────────────────
4087
+ console.log();
4088
+ console.log(bold(" Claude Code Hooks"));
4089
+ const settingsPath = claudeSettingsPath();
4090
+ if (existsSync(settingsPath)) {
4091
+ try {
4092
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
4093
+ const hooks = settings.hooks || {};
4094
+ let hookCount = 0;
4095
+ let staleHookCount = 0;
4096
+
4097
+ // Check recall hook
4098
+ const recallHooks = (hooks.UserPromptSubmit || []).filter((h) =>
4099
+ h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
4100
+ );
4101
+ if (recallHooks.length > 0) {
4102
+ console.log(` ${green("+")} Recall hook (UserPromptSubmit)`);
4103
+ hookCount++;
4104
+ }
4105
+
4106
+ // Check session-end hooks
4107
+ const sessionHooks = (hooks.SessionEnd || []).filter((h) =>
4108
+ h.hooks?.some(
4109
+ (hh) =>
4110
+ hh.command?.includes("session-end.mjs") ||
4111
+ hh.command?.includes("context-vault session-end"),
4112
+ ),
4113
+ );
4114
+ if (sessionHooks.length > 0) {
4115
+ // Check if using stale absolute path
4116
+ const hasStale = sessionHooks.some((h) =>
4117
+ h.hooks?.some(
4118
+ (hh) =>
4119
+ hh.command?.includes("session-end.mjs") &&
4120
+ !hh.command?.includes("context-vault session-end"),
4121
+ ),
4122
+ );
4123
+ if (hasStale) {
4124
+ const cmd = sessionHooks[0]?.hooks?.[0]?.command || "";
4125
+ const pathMatch = cmd.match(/node\s+(.+session-end\.mjs)/);
4126
+ const hookPath = pathMatch ? pathMatch[1] : "";
4127
+ const pathExists = hookPath && existsSync(hookPath);
4128
+ if (!pathExists) {
4129
+ console.log(
4130
+ ` ${red("✘")} Session capture hook: stale path ${dim(hookPath || "(unknown)")}`,
4131
+ );
4132
+ console.log(
4133
+ ` ${dim("Fix: run context-vault hooks install to update")}`,
4134
+ );
4135
+ staleHookCount++;
4136
+ allOk = false;
4137
+ } else {
4138
+ console.log(
4139
+ ` ${yellow("!")} Session capture hook: uses absolute path (fragile)`,
4140
+ );
4141
+ console.log(
4142
+ ` ${dim("Fix: run context-vault hooks install to update to CLI command")}`,
4143
+ );
4144
+ }
4145
+ } else {
4146
+ console.log(` ${green("+")} Session capture hook (SessionEnd)`);
4147
+ }
4148
+ hookCount++;
4149
+ }
4150
+
4151
+ // Check flush hook
4152
+ const flushHooks = (hooks.SessionEnd || []).filter((h) =>
4153
+ h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
4154
+ );
4155
+ if (flushHooks.length > 0) {
4156
+ console.log(` ${green("+")} Flush hook (SessionEnd)`);
4157
+ hookCount++;
4158
+ }
4159
+
4160
+ // Check post-tool-call hooks
4161
+ const ptcHooks = (hooks.PostToolCall || []).filter((h) =>
4162
+ h.hooks?.some(
4163
+ (hh) =>
4164
+ hh.command?.includes("post-tool-call.mjs") ||
4165
+ hh.command?.includes("context-vault post-tool-call"),
4166
+ ),
4167
+ );
4168
+ if (ptcHooks.length > 0) {
4169
+ const hasStale = ptcHooks.some((h) =>
4170
+ h.hooks?.some(
4171
+ (hh) =>
4172
+ hh.command?.includes("post-tool-call.mjs") &&
4173
+ !hh.command?.includes("context-vault post-tool-call"),
4174
+ ),
4175
+ );
4176
+ if (hasStale) {
4177
+ const cmd = ptcHooks[0]?.hooks?.[0]?.command || "";
4178
+ const pathMatch = cmd.match(/node\s+(.+post-tool-call\.mjs)/);
4179
+ const hookPath = pathMatch ? pathMatch[1] : "";
4180
+ const pathExists = hookPath && existsSync(hookPath);
4181
+ if (!pathExists) {
4182
+ console.log(
4183
+ ` ${red("✘")} Auto-capture hook: stale path ${dim(hookPath || "(unknown)")}`,
4184
+ );
4185
+ console.log(
4186
+ ` ${dim("Fix: run context-vault hooks install to update")}`,
4187
+ );
4188
+ staleHookCount++;
4189
+ allOk = false;
4190
+ } else {
4191
+ console.log(
4192
+ ` ${yellow("!")} Auto-capture hook: uses absolute path (fragile)`,
4193
+ );
4194
+ console.log(
4195
+ ` ${dim("Fix: run context-vault hooks install to update to CLI command")}`,
4196
+ );
4197
+ }
4198
+ } else {
4199
+ console.log(` ${green("+")} Auto-capture hook (PostToolCall)`);
4200
+ }
4201
+ hookCount++;
4202
+ }
4203
+
4204
+ if (hookCount === 0) {
4205
+ console.log(` ${dim("-")} No context-vault hooks installed`);
4206
+ console.log(` ${dim("Optional: run context-vault hooks install")}`);
4207
+ }
4208
+ } catch {
4209
+ console.log(` ${yellow("!")} Could not read ${settingsPath}`);
4210
+ }
4211
+ } else {
4212
+ console.log(` ${dim("-")} No Claude Code settings found`);
4213
+ }
4214
+
3768
4215
  // ── Summary ───────────────────────────────────────────────────────────────
3769
4216
  console.log();
3770
4217
  if (allOk) {
3771
4218
  console.log(
3772
- ` ${green("All checks passed.")} If the MCP server still fails, try:`,
3773
- );
3774
- console.log(
3775
- ` ${dim("context-vault setup")} — reconfigure tool integrations`,
4219
+ ` ${green("All checks passed.")} If the MCP server still fails, try restarting your AI tool.`,
3776
4220
  );
3777
4221
  } else {
3778
4222
  console.log(
3779
- ` ${yellow("Some issues found.")} Address the items above, then restart your AI tool.`,
3780
- );
3781
- console.log(
3782
- ` ${dim("context-vault setup")} — reconfigure and repair installation`,
4223
+ ` ${yellow("Some issues found.")} Address the items marked with ${red("✘")} above.`,
3783
4224
  );
3784
4225
  }
3785
4226
  console.log();
@@ -4117,7 +4558,7 @@ async function main() {
4117
4558
  }
4118
4559
 
4119
4560
  if (flags.has("--help") || command === "help") {
4120
- showHelp();
4561
+ showHelp(flags.has("--all"));
4121
4562
  return;
4122
4563
  }
4123
4564
 
@@ -4162,6 +4603,12 @@ async function main() {
4162
4603
  case "session-capture":
4163
4604
  await runSessionCapture();
4164
4605
  break;
4606
+ case "session-end":
4607
+ await runSessionEnd();
4608
+ break;
4609
+ case "post-tool-call":
4610
+ await runPostToolCall();
4611
+ break;
4165
4612
  case "save":
4166
4613
  await runSave();
4167
4614
  break;
@@ -4183,6 +4630,9 @@ async function main() {
4183
4630
  case "reindex":
4184
4631
  await runReindex();
4185
4632
  break;
4633
+ case "migrate-dirs":
4634
+ await runMigrateDirs();
4635
+ break;
4186
4636
  case "prune":
4187
4637
  await runPrune();
4188
4638
  break;