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 +454 -33
- 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 +9 -2
- 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 +19 -0
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
|
-
|
|
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
|
-
|
|
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(
|
|
3231
|
+
h.hooks?.some((hh) => hh.command?.includes(newCommand)),
|
|
3128
3232
|
);
|
|
3129
3233
|
if (alreadyInstalled) return false;
|
|
3130
3234
|
|
|
3131
|
-
|
|
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:
|
|
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) =>
|
|
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(
|
|
3319
|
+
h.hooks?.some((hh) => hh.command?.includes(newCommand)),
|
|
3199
3320
|
);
|
|
3200
3321
|
if (alreadyInstalled) return false;
|
|
3201
3322
|
|
|
3202
|
-
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
3607
|
-
db.
|
|
3608
|
-
|
|
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}" (
|
|
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:
|
|
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("
|
|
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
|
|
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;
|
|
@@ -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 = {};
|
|
@@ -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
|
-
|
|
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
|
@@ -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 ────────────────────────────────────────────────────
|