context-vault 2.16.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
@@ -305,6 +305,8 @@ ${bold("Commands:")}
305
305
  ${cyan("flush")} Check vault health and confirm DB is accessible
306
306
  ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
307
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)
308
310
  ${cyan("save")} Save an entry to the vault from CLI
309
311
  ${cyan("search")} Search vault entries from CLI
310
312
  ${cyan("reindex")} Rebuild search index from knowledge files
@@ -1783,7 +1785,18 @@ async function runStatus() {
1783
1785
  } catch {}
1784
1786
  }
1785
1787
 
1786
- 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
+ }
1787
1800
 
1788
1801
  const status = gatherVaultStatus({ db, config });
1789
1802
 
@@ -1956,6 +1969,25 @@ async function runUninstall() {
1956
1969
  }
1957
1970
  }
1958
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
+
1959
1991
  // Optionally remove data directory
1960
1992
  const dataDir = join(HOME, ".context-mcp");
1961
1993
  if (existsSync(dataDir)) {
@@ -2745,6 +2777,16 @@ async function runSessionCapture() {
2745
2777
  }
2746
2778
  }
2747
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
+
2748
2790
  async function runSave() {
2749
2791
  const kind = getFlag("--kind");
2750
2792
  const title = getFlag("--title");
@@ -3160,14 +3202,6 @@ function removeClaudeHook() {
3160
3202
  return true;
3161
3203
  }
3162
3204
 
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
3205
  /**
3172
3206
  * Writes a SessionEnd hook entry for session capture to ~/.claude/settings.json.
3173
3207
  * Returns true if installed, false if already present.
@@ -3190,17 +3224,29 @@ function installSessionCaptureHook() {
3190
3224
  if (!settings.hooks) settings.hooks = {};
3191
3225
  if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
3192
3226
 
3227
+ const newCommand = "context-vault session-end";
3228
+
3229
+ // Check if already installed with new CLI-based command
3193
3230
  const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
3194
- h.hooks?.some((hh) => hh.command?.includes("session-end.mjs")),
3231
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3195
3232
  );
3196
3233
  if (alreadyInstalled) return false;
3197
3234
 
3198
- 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
+
3199
3245
  settings.hooks.SessionEnd.push({
3200
3246
  hooks: [
3201
3247
  {
3202
3248
  type: "command",
3203
- command: `node ${hookScript}`,
3249
+ command: newCommand,
3204
3250
  timeout: 30,
3205
3251
  },
3206
3252
  ],
@@ -3230,7 +3276,12 @@ function removeSessionCaptureHook() {
3230
3276
 
3231
3277
  const before = settings.hooks.SessionEnd.length;
3232
3278
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
3233
- (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
+ ),
3234
3285
  );
3235
3286
 
3236
3287
  if (settings.hooks.SessionEnd.length === before) return false;
@@ -3261,17 +3312,29 @@ function installPostToolCallHook() {
3261
3312
  if (!settings.hooks) settings.hooks = {};
3262
3313
  if (!settings.hooks.PostToolCall) settings.hooks.PostToolCall = [];
3263
3314
 
3315
+ const newCommand = "context-vault post-tool-call";
3316
+
3317
+ // Check if already installed with new CLI-based command
3264
3318
  const alreadyInstalled = settings.hooks.PostToolCall.some((h) =>
3265
- h.hooks?.some((hh) => hh.command?.includes("post-tool-call.mjs")),
3319
+ h.hooks?.some((hh) => hh.command?.includes(newCommand)),
3266
3320
  );
3267
3321
  if (alreadyInstalled) return false;
3268
3322
 
3269
- 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
+
3270
3333
  settings.hooks.PostToolCall.push({
3271
3334
  hooks: [
3272
3335
  {
3273
3336
  type: "command",
3274
- command: `node ${hookScript}`,
3337
+ command: newCommand,
3275
3338
  timeout: 5,
3276
3339
  },
3277
3340
  ],
@@ -3301,7 +3364,12 @@ function removePostToolCallHook() {
3301
3364
 
3302
3365
  const before = settings.hooks.PostToolCall.length;
3303
3366
  settings.hooks.PostToolCall = settings.hooks.PostToolCall.filter(
3304
- (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
+ ),
3305
3373
  );
3306
3374
 
3307
3375
  if (settings.hooks.PostToolCall.length === before) return false;
@@ -3667,16 +3735,20 @@ async function runDoctor() {
3667
3735
  }
3668
3736
 
3669
3737
  // ── Database ──────────────────────────────────────────────────────────
3738
+ let db;
3670
3739
  if (existsSync(config.dbPath)) {
3671
3740
  try {
3672
3741
  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)}`);
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
+ );
3676
3748
  } catch (e) {
3677
3749
  console.log(` ${red("✘")} Database error: ${e.message}`);
3678
3750
  console.log(
3679
- ` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
3751
+ ` ${dim(`Fix: rm "${config.dbPath}" and restart (will rebuild from vault files)`)}`,
3680
3752
  );
3681
3753
  allOk = false;
3682
3754
  }
@@ -3686,6 +3758,101 @@ async function runDoctor() {
3686
3758
  );
3687
3759
  }
3688
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
+
3689
3856
  // ── Launcher (server.mjs) ─────────────────────────────────────────────
3690
3857
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
3691
3858
  if (existsSync(launcherPath)) {
@@ -3743,6 +3910,9 @@ async function runDoctor() {
3743
3910
  // ── MCP tool configs ──────────────────────────────────────────────────────
3744
3911
  console.log();
3745
3912
  console.log(bold(" Tool Configurations"));
3913
+ let anyToolConfigured = false;
3914
+
3915
+ // Check Claude Code
3746
3916
  const claudeConfigPath = join(HOME, ".claude.json");
3747
3917
  if (existsSync(claudeConfigPath)) {
3748
3918
  try {
@@ -3752,9 +3922,9 @@ async function runDoctor() {
3752
3922
  const srv = servers["context-vault"];
3753
3923
  const cmd = [srv.command, ...(srv.args || [])].join(" ");
3754
3924
  console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
3925
+ anyToolConfigured = true;
3755
3926
  } else {
3756
- console.log(` ${dim("-")} Claude Code: context-vault not configured`);
3757
- console.log(` ${dim("Fix: run context-vault setup")}`);
3927
+ console.log(` ${dim("-")} Claude Code: not configured`);
3758
3928
  }
3759
3929
  } catch {
3760
3930
  console.log(
@@ -3765,21 +3935,199 @@ async function runDoctor() {
3765
3935
  console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
3766
3936
  }
3767
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
+
3768
4122
  // ── Summary ───────────────────────────────────────────────────────────────
3769
4123
  console.log();
3770
4124
  if (allOk) {
3771
4125
  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`,
4126
+ ` ${green("All checks passed.")} If the MCP server still fails, try restarting your AI tool.`,
3776
4127
  );
3777
4128
  } else {
3778
4129
  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`,
4130
+ ` ${yellow("Some issues found.")} Address the items marked with ${red("✘")} above.`,
3783
4131
  );
3784
4132
  }
3785
4133
  console.log();
@@ -4162,6 +4510,12 @@ async function main() {
4162
4510
  case "session-capture":
4163
4511
  await runSessionCapture();
4164
4512
  break;
4513
+ case "session-end":
4514
+ await runSessionEnd();
4515
+ break;
4516
+ case "post-tool-call":
4517
+ await runPostToolCall();
4518
+ break;
4165
4519
  case "save":
4166
4520
  await runSave();
4167
4521
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.16.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 = {};
@@ -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.16.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);
@@ -150,6 +150,7 @@ async function main() {
150
150
  `[context-vault] Config reloaded: vaultDir changed to ${fresh.vaultDir}`,
151
151
  );
152
152
  lastVaultDir = fresh.vaultDir;
153
+ fresh.vaultDirExists = existsSync(fresh.vaultDir);
153
154
  }
154
155
  return fresh;
155
156
  },