context-vault 2.15.0 → 2.17.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/bin/cli.js CHANGED
@@ -64,6 +64,7 @@ function writeMarkerFile(vaultDir) {
64
64
 
65
65
  function scanForVaults() {
66
66
  const candidates = [
67
+ join(HOME, ".vault"),
67
68
  join(HOME, "vault"),
68
69
  join(HOME, "omni", "vault"),
69
70
  process.cwd(),
@@ -304,6 +305,8 @@ ${bold("Commands:")}
304
305
  ${cyan("flush")} Check vault health and confirm DB is accessible
305
306
  ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
306
307
  ${cyan("session-capture")} Save a session summary entry (reads JSON from stdin)
308
+ ${cyan("session-end")} Run session-end hook (parse transcript + capture)
309
+ ${cyan("post-tool-call")} Run post-tool-call hook (log tool usage)
307
310
  ${cyan("save")} Save an entry to the vault from CLI
308
311
  ${cyan("search")} Search vault entries from CLI
309
312
  ${cyan("reindex")} Rebuild search index from knowledge files
@@ -324,6 +327,7 @@ ${bold("Options:")}
324
327
  --version Show version
325
328
  --vault-dir <path> Set vault directory (setup/serve)
326
329
  --yes Non-interactive mode (accept all defaults)
330
+ --force Overwrite existing config without confirmation
327
331
  --skip-embeddings Skip embedding model download (FTS-only mode)
328
332
  `);
329
333
  }
@@ -538,7 +542,21 @@ async function runSetup() {
538
542
  console.log(dim(` [2/6]`) + bold(" Configuring vault...\n"));
539
543
 
540
544
  // Scan for existing vaults via marker file
541
- let defaultVaultDir = getFlag("--vault-dir") || join(HOME, "vault");
545
+ let defaultVaultDir = getFlag("--vault-dir") || join(HOME, ".vault");
546
+
547
+ // Prefer existing config vaultDir over default (prevents accidental overwrite)
548
+ if (!getFlag("--vault-dir")) {
549
+ const existingCfgPath = join(HOME, ".context-mcp", "config.json");
550
+ if (existsSync(existingCfgPath)) {
551
+ try {
552
+ const cfg = JSON.parse(readFileSync(existingCfgPath, "utf-8"));
553
+ if (cfg.vaultDir && existsSync(resolve(cfg.vaultDir))) {
554
+ defaultVaultDir = cfg.vaultDir;
555
+ }
556
+ } catch {}
557
+ }
558
+ }
559
+
542
560
  if (!getFlag("--vault-dir") && !isNonInteractive) {
543
561
  const existingVaults = scanForVaults();
544
562
  if (existingVaults.length === 1) {
@@ -576,7 +594,7 @@ async function runSetup() {
576
594
  const vaultDir = isNonInteractive
577
595
  ? defaultVaultDir
578
596
  : await prompt(` Vault directory:`, defaultVaultDir);
579
- const resolvedVaultDir = resolve(vaultDir);
597
+ let resolvedVaultDir = resolve(vaultDir);
580
598
 
581
599
  // Guard: vault dir path must not be an existing file
582
600
  if (existsSync(resolvedVaultDir)) {
@@ -626,6 +644,57 @@ async function runSetup() {
626
644
  Object.assign(vaultConfig, JSON.parse(readFileSync(configPath, "utf-8")));
627
645
  } catch {}
628
646
  }
647
+
648
+ const existingVaultDir = vaultConfig.vaultDir;
649
+ if (
650
+ existingVaultDir &&
651
+ resolve(existingVaultDir) !== resolvedVaultDir &&
652
+ !flags.has("--force")
653
+ ) {
654
+ let entryCount = 0;
655
+ try {
656
+ const knowledgeDir = join(resolve(existingVaultDir), "knowledge");
657
+ if (existsSync(knowledgeDir)) {
658
+ const countMd = (d) => {
659
+ let n = 0;
660
+ for (const e of readdirSync(d, { withFileTypes: true })) {
661
+ if (e.isDirectory()) n += countMd(join(d, e.name));
662
+ else if (e.name.endsWith(".md")) n++;
663
+ }
664
+ return n;
665
+ };
666
+ entryCount = countMd(knowledgeDir);
667
+ }
668
+ } catch {}
669
+
670
+ console.log();
671
+ console.log(
672
+ yellow(` ⚠ Existing config points to: ${resolve(existingVaultDir)}`) +
673
+ (entryCount > 0 ? dim(` (${entryCount} entries)`) : ""),
674
+ );
675
+ console.log(` Setup would change vaultDir to: ${resolvedVaultDir}`);
676
+
677
+ if (isNonInteractive) {
678
+ console.log();
679
+ console.log(
680
+ red(" Refusing to overwrite vaultDir in non-interactive mode."),
681
+ );
682
+ console.log(
683
+ dim(" Use --force to override, or --vault-dir to set explicitly."),
684
+ );
685
+ process.exit(1);
686
+ }
687
+
688
+ console.log();
689
+ const overwrite = await prompt(" Overwrite? (y/N):", "N");
690
+ if (overwrite.toLowerCase() !== "y" && overwrite.toLowerCase() !== "yes") {
691
+ console.log(
692
+ dim(` Keeping existing vaultDir: ${resolve(existingVaultDir)}`),
693
+ );
694
+ resolvedVaultDir = resolve(existingVaultDir);
695
+ }
696
+ }
697
+
629
698
  vaultConfig.vaultDir = resolvedVaultDir;
630
699
  vaultConfig.dataDir = dataDir;
631
700
  vaultConfig.dbPath = join(dataDir, "vault.db");
@@ -1716,7 +1785,18 @@ async function runStatus() {
1716
1785
  } catch {}
1717
1786
  }
1718
1787
 
1719
- const db = await initDatabase(config.dbPath);
1788
+ let db;
1789
+ try {
1790
+ db = await initDatabase(config.dbPath);
1791
+ } catch (e) {
1792
+ console.log();
1793
+ console.log(` ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}`);
1794
+ console.log();
1795
+ console.log(` ${red("✘")} Database not accessible: ${e.message}`);
1796
+ console.log(dim(` Run ${cyan("context-vault doctor")} for diagnostics`));
1797
+ console.log();
1798
+ process.exit(1);
1799
+ }
1720
1800
 
1721
1801
  const status = gatherVaultStatus({ db, config });
1722
1802
 
@@ -1889,6 +1969,25 @@ async function runUninstall() {
1889
1969
  }
1890
1970
  }
1891
1971
 
1972
+ // Remove Claude Code hooks
1973
+ const recallRemoved = removeClaudeHook();
1974
+ const captureRemoved = removeSessionCaptureHook();
1975
+ const flushRemoved = removeSessionEndHook();
1976
+ const autoCaptureRemoved = removePostToolCallHook();
1977
+ if (recallRemoved || captureRemoved || flushRemoved || autoCaptureRemoved) {
1978
+ console.log(` ${green("+")} Removed Claude Code hooks`);
1979
+ } else {
1980
+ console.log(` ${dim("-")} No Claude Code hooks to remove`);
1981
+ }
1982
+
1983
+ // Remove installed skills
1984
+ const skillsDir = join(HOME, ".claude", "skills", "compile-context");
1985
+ if (existsSync(skillsDir)) {
1986
+ const { rmSync } = await import("node:fs");
1987
+ rmSync(skillsDir, { recursive: true, force: true });
1988
+ console.log(` ${green("+")} Removed installed skills`);
1989
+ }
1990
+
1892
1991
  // Optionally remove data directory
1893
1992
  const dataDir = join(HOME, ".context-mcp");
1894
1993
  if (existsSync(dataDir)) {
@@ -2678,6 +2777,16 @@ async function runSessionCapture() {
2678
2777
  }
2679
2778
  }
2680
2779
 
2780
+ async function runSessionEnd() {
2781
+ const { main } = await import("../src/hooks/session-end.mjs");
2782
+ await main();
2783
+ }
2784
+
2785
+ async function runPostToolCall() {
2786
+ const { main } = await import("../src/hooks/post-tool-call.mjs");
2787
+ await main();
2788
+ }
2789
+
2681
2790
  async function runSave() {
2682
2791
  const kind = getFlag("--kind");
2683
2792
  const title = getFlag("--title");
@@ -3093,14 +3202,6 @@ function removeClaudeHook() {
3093
3202
  return true;
3094
3203
  }
3095
3204
 
3096
- function sessionEndHookPath() {
3097
- return resolve(ROOT, "src", "hooks", "session-end.mjs");
3098
- }
3099
-
3100
- function postToolCallHookPath() {
3101
- return resolve(ROOT, "src", "hooks", "post-tool-call.mjs");
3102
- }
3103
-
3104
3205
  /**
3105
3206
  * Writes a SessionEnd hook entry for session capture to ~/.claude/settings.json.
3106
3207
  * Returns true if installed, false if already present.
@@ -3123,17 +3224,29 @@ function installSessionCaptureHook() {
3123
3224
  if (!settings.hooks) settings.hooks = {};
3124
3225
  if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
3125
3226
 
3227
+ const newCommand = "context-vault session-end";
3228
+
3229
+ // Check if already installed with new CLI-based command
3126
3230
  const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
3127
- h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3231
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3128
3232
  );
3129
3233
  if (alreadyInstalled) return false;
3130
3234
 
3131
- const hookScript = sessionEndHookPath();
3235
+ // Migrate: remove stale absolute-path hooks (node <path>/session-end.mjs)
3236
+ const hadStale = settings.hooks.SessionEnd.some((h) =>
3237
+ h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3238
+ );
3239
+ if (hadStale) {
3240
+ settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
3241
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3242
+ );
3243
+ }
3244
+
3132
3245
  settings.hooks.SessionEnd.push({
3133
3246
  hooks: [
3134
3247
  {
3135
3248
  type: "command",
3136
- command: `node ${hookScript}`,
3249
+ command: newCommand,
3137
3250
  timeout: 30,
3138
3251
  },
3139
3252
  ],
@@ -3163,7 +3276,12 @@ function removeSessionCaptureHook() {
3163
3276
 
3164
3277
  const before = settings.hooks.SessionEnd.length;
3165
3278
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
3166
- (h) => !h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3279
+ (h) =>
3280
+ !h.hooks?.some(
3281
+ (hh) =>
3282
+ hh.command?.includes("session-end.mjs") ||
3283
+ hh.command?.includes("context-vault session-end"),
3284
+ ),
3167
3285
  );
3168
3286
 
3169
3287
  if (settings.hooks.SessionEnd.length === before) return false;
@@ -3194,17 +3312,29 @@ function installPostToolCallHook() {
3194
3312
  if (!settings.hooks) settings.hooks = {};
3195
3313
  if (!settings.hooks.PostToolCall) settings.hooks.PostToolCall = [];
3196
3314
 
3315
+ const newCommand = "context-vault post-tool-call";
3316
+
3317
+ // Check if already installed with new CLI-based command
3197
3318
  const alreadyInstalled = settings.hooks.PostToolCall.some((h) =>
3198
- h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3319
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3199
3320
  );
3200
3321
  if (alreadyInstalled) return false;
3201
3322
 
3202
- const hookScript = postToolCallHookPath();
3323
+ // Migrate: remove stale absolute-path hooks (node <path>/post-tool-call.mjs)
3324
+ const hadStale = settings.hooks.PostToolCall.some((h) =>
3325
+ h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3326
+ );
3327
+ if (hadStale) {
3328
+ settings.hooks.PostToolCall = settings.hooks.PostToolCall.filter(
3329
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3330
+ );
3331
+ }
3332
+
3203
3333
  settings.hooks.PostToolCall.push({
3204
3334
  hooks: [
3205
3335
  {
3206
3336
  type: "command",
3207
- command: `node ${hookScript}`,
3337
+ command: newCommand,
3208
3338
  timeout: 5,
3209
3339
  },
3210
3340
  ],
@@ -3234,7 +3364,12 @@ function removePostToolCallHook() {
3234
3364
 
3235
3365
  const before = settings.hooks.PostToolCall.length;
3236
3366
  settings.hooks.PostToolCall = settings.hooks.PostToolCall.filter(
3237
- (h) => !h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3367
+ (h) =>
3368
+ !h.hooks?.some(
3369
+ (hh) =>
3370
+ hh.command?.includes("post-tool-call.mjs") ||
3371
+ hh.command?.includes("context-vault post-tool-call"),
3372
+ ),
3238
3373
  );
3239
3374
 
3240
3375
  if (settings.hooks.PostToolCall.length === before) return false;
@@ -3600,16 +3735,20 @@ async function runDoctor() {
3600
3735
  }
3601
3736
 
3602
3737
  // ── Database ──────────────────────────────────────────────────────────
3738
+ let db;
3603
3739
  if (existsSync(config.dbPath)) {
3604
3740
  try {
3605
3741
  const { initDatabase } = await import("@context-vault/core/index/db");
3606
- const db = await initDatabase(config.dbPath);
3607
- db.close();
3608
- console.log(` ${green("")} Database ${dim(config.dbPath)}`);
3742
+ db = await initDatabase(config.dbPath);
3743
+ const schemaRow = db.prepare("PRAGMA user_version").get();
3744
+ const schemaVersion = schemaRow?.user_version ?? "unknown";
3745
+ console.log(
3746
+ ` ${green("✓")} Database ${dim(`${config.dbPath} (schema v${schemaVersion})`)}`,
3747
+ );
3609
3748
  } catch (e) {
3610
3749
  console.log(` ${red("✘")} Database error: ${e.message}`);
3611
3750
  console.log(
3612
- ` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
3751
+ ` ${dim(`Fix: rm "${config.dbPath}" and restart (will rebuild from vault files)`)}`,
3613
3752
  );
3614
3753
  allOk = false;
3615
3754
  }
@@ -3619,6 +3758,101 @@ async function runDoctor() {
3619
3758
  );
3620
3759
  }
3621
3760
 
3761
+ // ── Embedding model ──────────────────────────────────────────────────
3762
+ try {
3763
+ const { embed } = await import("@context-vault/core/index/embed");
3764
+ const vec = await embed("doctor check");
3765
+ if (vec && vec.length > 0) {
3766
+ console.log(
3767
+ ` ${green("✓")} Embedding model ${dim(`(${vec.length} dimensions)`)}`,
3768
+ );
3769
+ } else {
3770
+ console.log(
3771
+ ` ${yellow("!")} Embedding model unavailable — semantic search disabled (FTS-only)`,
3772
+ );
3773
+ console.log(
3774
+ ` ${dim("Fix: run context-vault setup to download the model")}`,
3775
+ );
3776
+ }
3777
+ } catch {
3778
+ console.log(
3779
+ ` ${yellow("!")} Embedding model unavailable — semantic search disabled (FTS-only)`,
3780
+ );
3781
+ console.log(
3782
+ ` ${dim("Fix: run context-vault setup to download the model")}`,
3783
+ );
3784
+ }
3785
+
3786
+ // ── DB/filesystem consistency ─────────────────────────────────────────
3787
+ if (db && existsSync(config.vaultDir)) {
3788
+ try {
3789
+ const totalRow = db.prepare("SELECT COUNT(*) as c FROM vault").get();
3790
+ const total = totalRow?.c ?? 0;
3791
+ if (total > 0) {
3792
+ const sampleRows = db
3793
+ .prepare("SELECT file_path FROM vault LIMIT 50")
3794
+ .all();
3795
+ let staleCount = 0;
3796
+ for (const row of sampleRows) {
3797
+ if (row.file_path && !existsSync(row.file_path)) {
3798
+ staleCount++;
3799
+ }
3800
+ }
3801
+ if (staleCount > 0) {
3802
+ const pct = Math.round((staleCount / sampleRows.length) * 100);
3803
+ console.log(
3804
+ ` ${yellow("!")} ${staleCount}/${sampleRows.length} sampled DB entries point to missing files (${pct}%)`,
3805
+ );
3806
+ console.log(
3807
+ ` ${dim("Fix: run context-vault reindex to rebuild from vault files")}`,
3808
+ );
3809
+ allOk = false;
3810
+ } else {
3811
+ console.log(
3812
+ ` ${green("✓")} DB/filesystem consistency ${dim(`(${total} entries, sample OK)`)}`,
3813
+ );
3814
+ }
3815
+ }
3816
+ } catch {
3817
+ // non-critical — skip silently
3818
+ }
3819
+ }
3820
+
3821
+ // ── Auto-captured feedback entries ─────────────────────────────────────
3822
+ if (db) {
3823
+ try {
3824
+ const feedbackRow = db
3825
+ .prepare(
3826
+ `SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%'`,
3827
+ )
3828
+ .get();
3829
+ const feedbackCount = feedbackRow?.c ?? 0;
3830
+ if (feedbackCount > 0) {
3831
+ const recentRows = db
3832
+ .prepare(
3833
+ `SELECT title, created_at FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%' ORDER BY created_at DESC LIMIT 3`,
3834
+ )
3835
+ .all();
3836
+ console.log(
3837
+ ` ${yellow("!")} ${feedbackCount} auto-captured error${feedbackCount === 1 ? "" : "s"} in vault`,
3838
+ );
3839
+ for (const row of recentRows) {
3840
+ console.log(` ${dim(`${row.created_at} — ${row.title}`)}`);
3841
+ }
3842
+ console.log(
3843
+ ` ${dim('Review: context-vault search --kind feedback --tag auto-captured')}`,
3844
+ );
3845
+ }
3846
+ } catch {
3847
+ // non-critical — skip silently
3848
+ }
3849
+ }
3850
+
3851
+ // Close DB if opened
3852
+ try {
3853
+ db?.close();
3854
+ } catch {}
3855
+
3622
3856
  // ── Launcher (server.mjs) ─────────────────────────────────────────────
3623
3857
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
3624
3858
  if (existsSync(launcherPath)) {
@@ -3676,6 +3910,9 @@ async function runDoctor() {
3676
3910
  // ── MCP tool configs ──────────────────────────────────────────────────────
3677
3911
  console.log();
3678
3912
  console.log(bold(" Tool Configurations"));
3913
+ let anyToolConfigured = false;
3914
+
3915
+ // Check Claude Code
3679
3916
  const claudeConfigPath = join(HOME, ".claude.json");
3680
3917
  if (existsSync(claudeConfigPath)) {
3681
3918
  try {
@@ -3685,9 +3922,9 @@ async function runDoctor() {
3685
3922
  const srv = servers["context-vault"];
3686
3923
  const cmd = [srv.command, ...(srv.args || [])].join(" ");
3687
3924
  console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
3925
+ anyToolConfigured = true;
3688
3926
  } else {
3689
- console.log(` ${dim("-")} Claude Code: context-vault not configured`);
3690
- console.log(` ${dim("Fix: run context-vault setup")}`);
3927
+ console.log(` ${dim("-")} Claude Code: not configured`);
3691
3928
  }
3692
3929
  } catch {
3693
3930
  console.log(
@@ -3698,21 +3935,199 @@ async function runDoctor() {
3698
3935
  console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
3699
3936
  }
3700
3937
 
3938
+ // Check all JSON-configured tools
3939
+ for (const tool of TOOLS.filter((t) => t.configType === "json")) {
3940
+ const cfgPath = tool.configPath;
3941
+ if (!cfgPath || !existsSync(cfgPath)) {
3942
+ continue; // tool not installed — skip silently
3943
+ }
3944
+ try {
3945
+ const toolConfig = JSON.parse(readFileSync(cfgPath, "utf-8"));
3946
+ const servers = toolConfig?.[tool.configKey] || {};
3947
+ if (servers["context-vault"]) {
3948
+ const srv = servers["context-vault"];
3949
+ const cmd = [srv.command, ...(srv.args || [])].join(" ");
3950
+ console.log(` ${green("+")} ${tool.name}: ${dim(cmd)}`);
3951
+ anyToolConfigured = true;
3952
+ } else if (servers["context-mcp"]) {
3953
+ console.log(
3954
+ ` ${yellow("!")} ${tool.name}: using old name "context-mcp"`,
3955
+ );
3956
+ console.log(
3957
+ ` ${dim("Fix: run context-vault setup to update")}`,
3958
+ );
3959
+ anyToolConfigured = true;
3960
+ }
3961
+ } catch {
3962
+ // config exists but unreadable — skip
3963
+ }
3964
+ }
3965
+
3966
+ // Check Codex
3967
+ try {
3968
+ const codexCheck = execSync("codex mcp list 2>/dev/null", {
3969
+ encoding: "utf-8",
3970
+ stdio: ["pipe", "pipe", "pipe"],
3971
+ timeout: 5000,
3972
+ });
3973
+ if (codexCheck.includes("context-vault")) {
3974
+ console.log(` ${green("+")} Codex: ${dim("configured")}`);
3975
+ anyToolConfigured = true;
3976
+ }
3977
+ } catch {
3978
+ // codex not installed or not configured — skip
3979
+ }
3980
+
3981
+ if (!anyToolConfigured) {
3982
+ console.log(
3983
+ ` ${yellow("!")} No AI tools have context-vault configured`,
3984
+ );
3985
+ console.log(` ${dim("Fix: run context-vault setup")}`);
3986
+ allOk = false;
3987
+ }
3988
+
3989
+ // ── Claude Code hooks ──────────────────────────────────────────────────────
3990
+ console.log();
3991
+ console.log(bold(" Claude Code Hooks"));
3992
+ const settingsPath = claudeSettingsPath();
3993
+ if (existsSync(settingsPath)) {
3994
+ try {
3995
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
3996
+ const hooks = settings.hooks || {};
3997
+ let hookCount = 0;
3998
+ let staleHookCount = 0;
3999
+
4000
+ // Check recall hook
4001
+ const recallHooks = (hooks.UserPromptSubmit || []).filter((h) =>
4002
+ h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
4003
+ );
4004
+ if (recallHooks.length > 0) {
4005
+ console.log(` ${green("+")} Recall hook (UserPromptSubmit)`);
4006
+ hookCount++;
4007
+ }
4008
+
4009
+ // Check session-end hooks
4010
+ const sessionHooks = (hooks.SessionEnd || []).filter((h) =>
4011
+ h.hooks?.some(
4012
+ (hh) =>
4013
+ hh.command?.includes("session-end.mjs") ||
4014
+ hh.command?.includes("context-vault session-end"),
4015
+ ),
4016
+ );
4017
+ if (sessionHooks.length > 0) {
4018
+ // Check if using stale absolute path
4019
+ const hasStale = sessionHooks.some((h) =>
4020
+ h.hooks?.some(
4021
+ (hh) =>
4022
+ hh.command?.includes("session-end.mjs") &&
4023
+ !hh.command?.includes("context-vault session-end"),
4024
+ ),
4025
+ );
4026
+ if (hasStale) {
4027
+ const cmd = sessionHooks[0]?.hooks?.[0]?.command || "";
4028
+ const pathMatch = cmd.match(/node\s+(.+session-end\.mjs)/);
4029
+ const hookPath = pathMatch ? pathMatch[1] : "";
4030
+ const pathExists = hookPath && existsSync(hookPath);
4031
+ if (!pathExists) {
4032
+ console.log(
4033
+ ` ${red("✘")} Session capture hook: stale path ${dim(hookPath || "(unknown)")}`,
4034
+ );
4035
+ console.log(
4036
+ ` ${dim("Fix: run context-vault hooks install to update")}`,
4037
+ );
4038
+ staleHookCount++;
4039
+ allOk = false;
4040
+ } else {
4041
+ console.log(
4042
+ ` ${yellow("!")} Session capture hook: uses absolute path (fragile)`,
4043
+ );
4044
+ console.log(
4045
+ ` ${dim("Fix: run context-vault hooks install to update to CLI command")}`,
4046
+ );
4047
+ }
4048
+ } else {
4049
+ console.log(` ${green("+")} Session capture hook (SessionEnd)`);
4050
+ }
4051
+ hookCount++;
4052
+ }
4053
+
4054
+ // Check flush hook
4055
+ const flushHooks = (hooks.SessionEnd || []).filter((h) =>
4056
+ h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
4057
+ );
4058
+ if (flushHooks.length > 0) {
4059
+ console.log(` ${green("+")} Flush hook (SessionEnd)`);
4060
+ hookCount++;
4061
+ }
4062
+
4063
+ // Check post-tool-call hooks
4064
+ const ptcHooks = (hooks.PostToolCall || []).filter((h) =>
4065
+ h.hooks?.some(
4066
+ (hh) =>
4067
+ hh.command?.includes("post-tool-call.mjs") ||
4068
+ hh.command?.includes("context-vault post-tool-call"),
4069
+ ),
4070
+ );
4071
+ if (ptcHooks.length > 0) {
4072
+ const hasStale = ptcHooks.some((h) =>
4073
+ h.hooks?.some(
4074
+ (hh) =>
4075
+ hh.command?.includes("post-tool-call.mjs") &&
4076
+ !hh.command?.includes("context-vault post-tool-call"),
4077
+ ),
4078
+ );
4079
+ if (hasStale) {
4080
+ const cmd = ptcHooks[0]?.hooks?.[0]?.command || "";
4081
+ const pathMatch = cmd.match(/node\s+(.+post-tool-call\.mjs)/);
4082
+ const hookPath = pathMatch ? pathMatch[1] : "";
4083
+ const pathExists = hookPath && existsSync(hookPath);
4084
+ if (!pathExists) {
4085
+ console.log(
4086
+ ` ${red("✘")} Auto-capture hook: stale path ${dim(hookPath || "(unknown)")}`,
4087
+ );
4088
+ console.log(
4089
+ ` ${dim("Fix: run context-vault hooks install to update")}`,
4090
+ );
4091
+ staleHookCount++;
4092
+ allOk = false;
4093
+ } else {
4094
+ console.log(
4095
+ ` ${yellow("!")} Auto-capture hook: uses absolute path (fragile)`,
4096
+ );
4097
+ console.log(
4098
+ ` ${dim("Fix: run context-vault hooks install to update to CLI command")}`,
4099
+ );
4100
+ }
4101
+ } else {
4102
+ console.log(` ${green("+")} Auto-capture hook (PostToolCall)`);
4103
+ }
4104
+ hookCount++;
4105
+ }
4106
+
4107
+ if (hookCount === 0) {
4108
+ console.log(` ${dim("-")} No context-vault hooks installed`);
4109
+ console.log(
4110
+ ` ${dim("Optional: run context-vault hooks install")}`,
4111
+ );
4112
+ }
4113
+ } catch {
4114
+ console.log(
4115
+ ` ${yellow("!")} Could not read ${settingsPath}`,
4116
+ );
4117
+ }
4118
+ } else {
4119
+ console.log(` ${dim("-")} No Claude Code settings found`);
4120
+ }
4121
+
3701
4122
  // ── Summary ───────────────────────────────────────────────────────────────
3702
4123
  console.log();
3703
4124
  if (allOk) {
3704
4125
  console.log(
3705
- ` ${green("All checks passed.")} If the MCP server still fails, try:`,
3706
- );
3707
- console.log(
3708
- ` ${dim("context-vault setup")} — reconfigure tool integrations`,
4126
+ ` ${green("All checks passed.")} If the MCP server still fails, try restarting your AI tool.`,
3709
4127
  );
3710
4128
  } else {
3711
4129
  console.log(
3712
- ` ${yellow("Some issues found.")} Address the items above, then restart your AI tool.`,
3713
- );
3714
- console.log(
3715
- ` ${dim("context-vault setup")} — reconfigure and repair installation`,
4130
+ ` ${yellow("Some issues found.")} Address the items marked with ${red("✘")} above.`,
3716
4131
  );
3717
4132
  }
3718
4133
  console.log();
@@ -4095,6 +4510,12 @@ async function main() {
4095
4510
  case "session-capture":
4096
4511
  await runSessionCapture();
4097
4512
  break;
4513
+ case "session-end":
4514
+ await runSessionEnd();
4515
+ break;
4516
+ case "post-tool-call":
4517
+ await runPostToolCall();
4518
+ break;
4098
4519
  case "save":
4099
4520
  await runSave();
4100
4521
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.15.0",
3
+ "version": "2.17.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",
@@ -14,8 +14,13 @@ export const MAX_SOURCE_LENGTH = 200;
14
14
  export const MAX_IDENTITY_KEY_LENGTH = 200;
15
15
 
16
16
  export const DEFAULT_GROWTH_THRESHOLDS = {
17
- totalEntries: { warn: 1000, critical: 5000 },
18
- eventEntries: { warn: 500, critical: 2000 },
17
+ totalEntries: { warn: 2000, critical: 5000 },
18
+ eventEntries: { warn: 1000, critical: 3000 },
19
19
  vaultSizeBytes: { warn: 50 * 1024 * 1024, critical: 200 * 1024 * 1024 },
20
20
  eventsWithoutTtl: { warn: 200 },
21
21
  };
22
+
23
+ export const DEFAULT_LIFECYCLE = {
24
+ event: { archiveAfterDays: 90 },
25
+ ephemeral: { archiveAfterDays: 30 },
26
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { DEFAULT_GROWTH_THRESHOLDS } from "../constants.js";
4
+ import { DEFAULT_GROWTH_THRESHOLDS, DEFAULT_LIFECYCLE } from "../constants.js";
5
5
 
6
6
  export function parseArgs(argv) {
7
7
  const args = {};
@@ -27,7 +27,7 @@ export function resolveConfig() {
27
27
  join(HOME, ".context-mcp"),
28
28
  );
29
29
  const config = {
30
- vaultDir: join(HOME, "vault"),
30
+ vaultDir: join(HOME, ".vault"),
31
31
  dataDir,
32
32
  dbPath: join(dataDir, "vault.db"),
33
33
  devDir: join(HOME, "dev"),
@@ -48,6 +48,7 @@ export function resolveConfig() {
48
48
  maxAgeDays: 7,
49
49
  autoConsolidate: false,
50
50
  },
51
+ lifecycle: structuredClone(DEFAULT_LIFECYCLE),
51
52
  };
52
53
 
53
54
  const configPath = join(dataDir, "config.json");
@@ -62,6 +63,12 @@ export function resolveConfig() {
62
63
  if (fc.dbPath) config.dbPath = fc.dbPath;
63
64
  if (fc.devDir) config.devDir = fc.devDir;
64
65
  if (fc.eventDecayDays != null) config.eventDecayDays = fc.eventDecayDays;
66
+ if (fc.growthWarningThreshold != null) {
67
+ config.thresholds.totalEntries = {
68
+ ...config.thresholds.totalEntries,
69
+ warn: Number(fc.growthWarningThreshold),
70
+ };
71
+ }
65
72
  if (fc.thresholds) {
66
73
  const t = fc.thresholds;
67
74
  if (t.totalEntries)
@@ -212,7 +212,7 @@ export function gatherVaultStatus(ctx, opts = {}) {
212
212
  *
213
213
  * @param {object} status — result of gatherVaultStatus()
214
214
  * @param {object} thresholds — from config.thresholds
215
- * @returns {{ warnings: Array, hasCritical: boolean, hasWarnings: boolean, actions: string[] }}
215
+ * @returns {{ warnings: Array, hasCritical: boolean, hasWarnings: boolean, actions: string[], kindBreakdown: Array }}
216
216
  */
217
217
  export function computeGrowthWarnings(status, thresholds) {
218
218
  if (!thresholds)
@@ -221,6 +221,7 @@ export function computeGrowthWarnings(status, thresholds) {
221
221
  hasCritical: false,
222
222
  hasWarnings: false,
223
223
  actions: [],
224
+ kindBreakdown: [],
224
225
  };
225
226
 
226
227
  const t = thresholds;
@@ -235,12 +236,16 @@ export function computeGrowthWarnings(status, thresholds) {
235
236
  dbSizeBytes = 0,
236
237
  } = status;
237
238
 
239
+ let totalExceeded = false;
240
+
238
241
  if (t.totalEntries?.critical != null && total >= t.totalEntries.critical) {
242
+ totalExceeded = true;
239
243
  warnings.push({
240
244
  level: "critical",
241
245
  message: `Total entries: ${total.toLocaleString()} (exceeds critical limit of ${t.totalEntries.critical.toLocaleString()})`,
242
246
  });
243
247
  } else if (t.totalEntries?.warn != null && total >= t.totalEntries.warn) {
248
+ totalExceeded = true;
244
249
  warnings.push({
245
250
  level: "warn",
246
251
  message: `Total entries: ${total.toLocaleString()} (exceeds recommended ${t.totalEntries.warn.toLocaleString()})`,
@@ -320,5 +325,26 @@ export function computeGrowthWarnings(status, thresholds) {
320
325
  actions.push("Consider archiving events older than 90 days");
321
326
  }
322
327
 
323
- return { warnings, hasCritical, hasWarnings: warnings.length > 0, actions };
328
+ const kindBreakdown = totalExceeded
329
+ ? buildKindBreakdown(status.kindCounts, total)
330
+ : [];
331
+
332
+ return {
333
+ warnings,
334
+ hasCritical,
335
+ hasWarnings: warnings.length > 0,
336
+ actions,
337
+ kindBreakdown,
338
+ };
339
+ }
340
+
341
+ function buildKindBreakdown(kindCounts, total) {
342
+ if (!kindCounts?.length || total === 0) return [];
343
+ return [...kindCounts]
344
+ .sort((a, b) => b.c - a.c)
345
+ .map(({ kind, c }) => ({
346
+ kind,
347
+ count: c,
348
+ pct: Math.round((c / total) * 100),
349
+ }));
324
350
  }
@@ -196,18 +196,20 @@ export async function indexEntry(
196
196
  );
197
197
  }
198
198
 
199
- // Embeddings are always generated from plaintext (before encryption)
200
- const embeddingText = [title, body].filter(Boolean).join(" ");
201
- const embedding = await ctx.embed(embeddingText);
199
+ // Skip embedding generation for event entries they are excluded from
200
+ // default semantic search and don't need vector representations
201
+ if (cat !== "event") {
202
+ const embeddingText = [title, body].filter(Boolean).join(" ");
203
+ const embedding = await ctx.embed(embeddingText);
202
204
 
203
- // Upsert vec: delete old if exists, then insert new (skip if embedding unavailable)
204
- if (embedding) {
205
- try {
206
- ctx.deleteVec(rowid);
207
- } catch {
208
- /* no-op if not found */
205
+ if (embedding) {
206
+ try {
207
+ ctx.deleteVec(rowid);
208
+ } catch {
209
+ /* no-op if not found */
210
+ }
211
+ ctx.insertVec(rowid, embedding);
209
212
  }
210
- ctx.insertVec(rowid, embedding);
211
213
  }
212
214
  }
213
215
 
@@ -370,15 +372,17 @@ export async function reindex(ctx, opts = {}) {
370
372
  fmMeta.updated || created,
371
373
  );
372
374
  if (result.changes > 0) {
373
- const rowidResult = ctx.stmts.getRowid.get(id);
374
- if (rowidResult?.rowid) {
375
- const embeddingText = [parsed.title, parsed.body]
376
- .filter(Boolean)
377
- .join(" ");
378
- pendingEmbeds.push({
379
- rowid: rowidResult.rowid,
380
- text: embeddingText,
381
- });
375
+ if (category !== "event") {
376
+ const rowidResult = ctx.stmts.getRowid.get(id);
377
+ if (rowidResult?.rowid) {
378
+ const embeddingText = [parsed.title, parsed.body]
379
+ .filter(Boolean)
380
+ .join(" ");
381
+ pendingEmbeds.push({
382
+ rowid: rowidResult.rowid,
383
+ text: embeddingText,
384
+ });
385
+ }
382
386
  }
383
387
  stats.added++;
384
388
  } else {
@@ -407,7 +411,7 @@ export async function reindex(ctx, opts = {}) {
407
411
  );
408
412
 
409
413
  // Queue re-embed if title or body changed (vector ops deferred to Phase 2)
410
- if (bodyChanged || titleChanged) {
414
+ if ((bodyChanged || titleChanged) && category !== "event") {
411
415
  const rowid = ctx.stmts.getRowid.get(existing.id)?.rowid;
412
416
  if (rowid) {
413
417
  const embeddingText = [parsed.title, parsed.body]
@@ -74,6 +74,7 @@ export function recencyBoost(createdAt, category, decayDays = 30) {
74
74
  */
75
75
  export function buildFilterClauses({
76
76
  categoryFilter,
77
+ excludeEvents = false,
77
78
  since,
78
79
  until,
79
80
  userIdFilter,
@@ -94,6 +95,9 @@ export function buildFilterClauses({
94
95
  clauses.push("e.category = ?");
95
96
  params.push(categoryFilter);
96
97
  }
98
+ if (excludeEvents && !categoryFilter) {
99
+ clauses.push("e.category != 'event'");
100
+ }
97
101
  if (since) {
98
102
  clauses.push("e.created_at >= ?");
99
103
  params.push(since);
@@ -242,6 +246,7 @@ export async function hybridSearch(
242
246
  {
243
247
  kindFilter = null,
244
248
  categoryFilter = null,
249
+ excludeEvents = false,
245
250
  since = null,
246
251
  until = null,
247
252
  limit = 20,
@@ -258,6 +263,7 @@ export async function hybridSearch(
258
263
 
259
264
  const extraFilters = buildFilterClauses({
260
265
  categoryFilter,
266
+ excludeEvents,
261
267
  since,
262
268
  until,
263
269
  userIdFilter,
@@ -340,6 +346,7 @@ export async function hybridSearch(
340
346
  if (teamIdFilter && row.team_id !== teamIdFilter) continue;
341
347
  if (kindFilter && row.kind !== kindFilter) continue;
342
348
  if (categoryFilter && row.category !== categoryFilter) continue;
349
+ if (excludeEvents && row.category === "event") continue;
343
350
  if (since && row.created_at < since) continue;
344
351
  if (until && row.created_at > until) continue;
345
352
  if (row.expires_at && new Date(row.expires_at) <= new Date())
@@ -146,6 +146,13 @@ export function handler(_args, ctx) {
146
146
  for (const w of growth.warnings) {
147
147
  lines.push(` ${w.message}`);
148
148
  }
149
+ if (growth.kindBreakdown.length) {
150
+ lines.push("");
151
+ lines.push(" Breakdown by kind:");
152
+ for (const { kind, count, pct } of growth.kindBreakdown) {
153
+ lines.push(` ${kind}: ${count.toLocaleString()} (${pct}%)`);
154
+ }
155
+ }
149
156
  if (growth.actions.length) {
150
157
  lines.push("", "Suggested growth actions:");
151
158
  for (const a of growth.actions) {
@@ -316,6 +316,12 @@ export const inputSchema = {
316
316
  .describe(
317
317
  "If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.",
318
318
  ),
319
+ include_events: z
320
+ .boolean()
321
+ .optional()
322
+ .describe(
323
+ "If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters.",
324
+ ),
319
325
  };
320
326
 
321
327
  /**
@@ -339,6 +345,7 @@ export async function handler(
339
345
  max_tokens,
340
346
  pivot_count,
341
347
  include_ephemeral,
348
+ include_events,
342
349
  },
343
350
  ctx,
344
351
  { ensureIndexed, reindexFailed },
@@ -347,6 +354,7 @@ export async function handler(
347
354
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
348
355
 
349
356
  const hasQuery = query?.trim();
357
+ const shouldExcludeEvents = hasQuery && !include_events && !category;
350
358
  // Expand buckets to bucket: prefixed tags and merge with explicit tags
351
359
  const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
352
360
  const effectiveTags = [...(tags ?? []), ...bucketTags];
@@ -413,6 +421,7 @@ export async function handler(
413
421
  const sorted = await hybridSearch(ctx, query, {
414
422
  kindFilter,
415
423
  categoryFilter: category || null,
424
+ excludeEvents: shouldExcludeEvents,
416
425
  since: effectiveSince,
417
426
  until: effectiveUntil,
418
427
  limit: fetchLimit,
@@ -490,6 +499,11 @@ export async function handler(
490
499
  filtered = filtered.filter((r) => r.tier !== "ephemeral");
491
500
  }
492
501
 
502
+ // Event category filter: exclude events from semantic search by default
503
+ if (shouldExcludeEvents) {
504
+ filtered = filtered.filter((r) => r.category !== "event");
505
+ }
506
+
493
507
  if (!filtered.length) {
494
508
  if (autoWindowed) {
495
509
  const days = config.eventDecayDays || 30;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.15.0",
3
+ "version": "2.17.0",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -56,7 +56,7 @@
56
56
  "@context-vault/core"
57
57
  ],
58
58
  "dependencies": {
59
- "@context-vault/core": "^2.15.0",
59
+ "@context-vault/core": "^2.17.0",
60
60
  "@modelcontextprotocol/sdk": "^1.26.0",
61
61
  "sqlite-vec": "^0.1.0"
62
62
  }
@@ -28,7 +28,7 @@ function extractRelevantInput(toolName, input) {
28
28
  return relevant;
29
29
  }
30
30
 
31
- async function main() {
31
+ export async function main() {
32
32
  try {
33
33
  const raw = await readStdin();
34
34
  if (!raw.trim()) process.exit(0);
@@ -428,7 +428,7 @@ export function buildSummary({
428
428
  return sections.join("\n");
429
429
  }
430
430
 
431
- async function main() {
431
+ export async function main() {
432
432
  try {
433
433
  const raw = await readStdin();
434
434
  if (!raw.trim()) process.exit(0);
@@ -138,6 +138,25 @@ async function main() {
138
138
  { capabilities: { tools: {} } },
139
139
  );
140
140
 
141
+ // Hot-reload config.json on every tool call (Option C from #144).
142
+ // resolveConfig() re-reads the small file each time — negligible I/O
143
+ // compared to DB queries and embedding operations that follow.
144
+ let lastVaultDir = config.vaultDir;
145
+ Object.defineProperty(ctx, "config", {
146
+ get() {
147
+ const fresh = resolveConfig();
148
+ if (fresh.vaultDir !== lastVaultDir) {
149
+ console.error(
150
+ `[context-vault] Config reloaded: vaultDir changed to ${fresh.vaultDir}`,
151
+ );
152
+ lastVaultDir = fresh.vaultDir;
153
+ fresh.vaultDirExists = existsSync(fresh.vaultDir);
154
+ }
155
+ return fresh;
156
+ },
157
+ configurable: true,
158
+ });
159
+
141
160
  registerTools(server, ctx);
142
161
 
143
162
  // ── Graceful Shutdown ────────────────────────────────────────────────────