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 +385 -31
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/constants.js +7 -2
- package/node_modules/@context-vault/core/src/core/config.js +8 -1
- package/node_modules/@context-vault/core/src/core/status.js +28 -2
- package/node_modules/@context-vault/core/src/index/index.js +24 -20
- package/node_modules/@context-vault/core/src/retrieve/index.js +7 -0
- package/node_modules/@context-vault/core/src/server/tools/context-status.js +7 -0
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +14 -0
- package/package.json +2 -2
- package/src/hooks/post-tool-call.mjs +1 -1
- package/src/hooks/session-end.mjs +1 -1
- package/src/server/index.js +1 -0
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
|
-
|
|
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(
|
|
3231
|
+
h.hooks?.some((hh) => hh.command?.includes(newCommand)),
|
|
3195
3232
|
);
|
|
3196
3233
|
if (alreadyInstalled) return false;
|
|
3197
3234
|
|
|
3198
|
-
|
|
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:
|
|
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) =>
|
|
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(
|
|
3319
|
+
h.hooks?.some((hh) => hh.command?.includes(newCommand)),
|
|
3266
3320
|
);
|
|
3267
3321
|
if (alreadyInstalled) return false;
|
|
3268
3322
|
|
|
3269
|
-
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
3674
|
-
db.
|
|
3675
|
-
|
|
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}" (
|
|
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:
|
|
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("
|
|
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
|
|
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;
|
|
@@ -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:
|
|
18
|
-
eventEntries: { warn:
|
|
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
|
-
|
|
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
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
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.
|
|
59
|
+
"@context-vault/core": "^2.17.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
61
61
|
"sqlite-vec": "^0.1.0"
|
|
62
62
|
}
|
package/src/server/index.js
CHANGED