context-vault 2.10.3 → 2.11.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 +345 -6
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/capture/file-ops.js +4 -0
- package/node_modules/@context-vault/core/src/capture/index.js +26 -0
- package/node_modules/@context-vault/core/src/constants.js +7 -0
- package/node_modules/@context-vault/core/src/core/categories.js +11 -0
- package/node_modules/@context-vault/core/src/core/config.js +32 -0
- package/node_modules/@context-vault/core/src/core/frontmatter.js +2 -0
- package/node_modules/@context-vault/core/src/core/status.js +165 -0
- package/node_modules/@context-vault/core/src/core/telemetry.js +64 -0
- package/node_modules/@context-vault/core/src/index/db.js +86 -9
- package/node_modules/@context-vault/core/src/index/index.js +37 -1
- package/node_modules/@context-vault/core/src/index.js +1 -1
- package/node_modules/@context-vault/core/src/retrieve/index.js +81 -4
- package/node_modules/@context-vault/core/src/server/tools/context-status.js +37 -3
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +23 -2
- package/node_modules/@context-vault/core/src/server/tools/ingest-url.js +0 -11
- package/node_modules/@context-vault/core/src/server/tools/list-context.js +7 -3
- package/node_modules/@context-vault/core/src/server/tools/save-context.js +138 -13
- package/node_modules/@context-vault/core/src/server/tools.js +13 -0
- package/package.json +2 -2
- package/src/server/index.js +27 -0
package/bin/cli.js
CHANGED
|
@@ -234,7 +234,10 @@ ${bold("Commands:")}
|
|
|
234
234
|
${cyan("connect")} --key cv_... Connect AI tools to hosted vault
|
|
235
235
|
${cyan("switch")} local|hosted Switch between local and hosted MCP modes
|
|
236
236
|
${cyan("serve")} Start the MCP server (used by AI clients)
|
|
237
|
+
${cyan("hooks")} install|remove Install or remove Claude Code memory hook
|
|
238
|
+
${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
|
|
237
239
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
240
|
+
${cyan("prune")} Remove expired entries (use --dry-run to preview)
|
|
238
241
|
${cyan("status")} Show vault diagnostics
|
|
239
242
|
${cyan("update")} Check for and install updates
|
|
240
243
|
${cyan("uninstall")} Remove MCP configs and optionally data
|
|
@@ -359,7 +362,7 @@ async function runSetup() {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
// Detect tools
|
|
362
|
-
console.log(dim(` [1/
|
|
365
|
+
console.log(dim(` [1/6]`) + bold(" Detecting tools...\n"));
|
|
363
366
|
const { detected, results: detectionResults } = await detectAllTools();
|
|
364
367
|
printDetectionResults(detectionResults);
|
|
365
368
|
console.log();
|
|
@@ -427,7 +430,7 @@ async function runSetup() {
|
|
|
427
430
|
}
|
|
428
431
|
|
|
429
432
|
// Vault directory (content files)
|
|
430
|
-
console.log(dim(` [2/
|
|
433
|
+
console.log(dim(` [2/6]`) + bold(" Configuring vault...\n"));
|
|
431
434
|
const defaultVaultDir = getFlag("--vault-dir") || join(HOME, "vault");
|
|
432
435
|
const vaultDir = isNonInteractive
|
|
433
436
|
? defaultVaultDir
|
|
@@ -484,6 +487,39 @@ async function runSetup() {
|
|
|
484
487
|
vaultConfig.dbPath = join(dataDir, "vault.db");
|
|
485
488
|
vaultConfig.devDir = join(HOME, "dev");
|
|
486
489
|
vaultConfig.mode = "local";
|
|
490
|
+
|
|
491
|
+
// Telemetry opt-in
|
|
492
|
+
console.log(`\n ${dim("[3/6]")}${bold(" Anonymous error reporting\n")}`);
|
|
493
|
+
console.log(
|
|
494
|
+
dim(
|
|
495
|
+
" When enabled, unhandled errors send a minimal event (type, tool name,",
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
console.log(
|
|
499
|
+
dim(" version, platform) to help diagnose issues. No vault content,"),
|
|
500
|
+
);
|
|
501
|
+
console.log(
|
|
502
|
+
dim(" file paths, or personal data is ever sent. Off by default."),
|
|
503
|
+
);
|
|
504
|
+
console.log(dim(" Full schema: https://contextvault.dev/telemetry"));
|
|
505
|
+
console.log();
|
|
506
|
+
|
|
507
|
+
let telemetryEnabled = vaultConfig.telemetry === true;
|
|
508
|
+
if (!isNonInteractive) {
|
|
509
|
+
const defaultChoice = telemetryEnabled ? "Y" : "n";
|
|
510
|
+
const telemetryAnswer = await prompt(
|
|
511
|
+
` Enable anonymous error reporting? (y/N):`,
|
|
512
|
+
defaultChoice,
|
|
513
|
+
);
|
|
514
|
+
telemetryEnabled =
|
|
515
|
+
telemetryAnswer.toLowerCase() === "y" ||
|
|
516
|
+
telemetryAnswer.toLowerCase() === "yes";
|
|
517
|
+
}
|
|
518
|
+
vaultConfig.telemetry = telemetryEnabled;
|
|
519
|
+
console.log(
|
|
520
|
+
` ${telemetryEnabled ? green("+") : dim("-")} Telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`,
|
|
521
|
+
);
|
|
522
|
+
|
|
487
523
|
writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
|
|
488
524
|
console.log(`\n ${green("+")} Wrote ${configPath}`);
|
|
489
525
|
|
|
@@ -491,7 +527,7 @@ async function runSetup() {
|
|
|
491
527
|
const skipEmbeddings = flags.has("--skip-embeddings");
|
|
492
528
|
if (skipEmbeddings) {
|
|
493
529
|
console.log(
|
|
494
|
-
`\n ${dim("[
|
|
530
|
+
`\n ${dim("[4/6]")}${bold(" Embedding model")} ${dim("(skipped)")}`,
|
|
495
531
|
);
|
|
496
532
|
console.log(
|
|
497
533
|
dim(
|
|
@@ -503,7 +539,7 @@ async function runSetup() {
|
|
|
503
539
|
);
|
|
504
540
|
} else {
|
|
505
541
|
console.log(
|
|
506
|
-
`\n ${dim("[
|
|
542
|
+
`\n ${dim("[4/6]")}${bold(" Downloading embedding model...")}`,
|
|
507
543
|
);
|
|
508
544
|
console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
|
|
509
545
|
{
|
|
@@ -592,7 +628,7 @@ async function runSetup() {
|
|
|
592
628
|
}
|
|
593
629
|
|
|
594
630
|
// Configure each tool — pass vault dir as arg if non-default
|
|
595
|
-
console.log(`\n ${dim("[
|
|
631
|
+
console.log(`\n ${dim("[5/6]")}${bold(" Configuring tools...\n")}`);
|
|
596
632
|
const results = [];
|
|
597
633
|
const defaultVDir = join(HOME, "vault");
|
|
598
634
|
const customVaultDir =
|
|
@@ -615,6 +651,47 @@ async function runSetup() {
|
|
|
615
651
|
}
|
|
616
652
|
}
|
|
617
653
|
|
|
654
|
+
// Claude Code memory hook (opt-in)
|
|
655
|
+
const claudeConfigured = results.some(
|
|
656
|
+
(r) => r.ok && r.tool.id === "claude-code",
|
|
657
|
+
);
|
|
658
|
+
const hookFlag = flags.has("--hooks");
|
|
659
|
+
if (claudeConfigured) {
|
|
660
|
+
let installHook = hookFlag;
|
|
661
|
+
if (!hookFlag && !isNonInteractive) {
|
|
662
|
+
console.log();
|
|
663
|
+
console.log(dim(" Claude Code detected — install memory hook?"));
|
|
664
|
+
console.log(
|
|
665
|
+
dim(
|
|
666
|
+
" Searches your vault on every prompt and injects relevant entries",
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
console.log(
|
|
670
|
+
dim(" as additional context alongside Claude's native memory."),
|
|
671
|
+
);
|
|
672
|
+
console.log();
|
|
673
|
+
const answer = await prompt(
|
|
674
|
+
" Install Claude Code memory hook? (y/N):",
|
|
675
|
+
"N",
|
|
676
|
+
);
|
|
677
|
+
installHook = answer.toLowerCase() === "y";
|
|
678
|
+
}
|
|
679
|
+
if (installHook) {
|
|
680
|
+
try {
|
|
681
|
+
const installed = installClaudeHook();
|
|
682
|
+
if (installed) {
|
|
683
|
+
console.log(`\n ${green("+")} Memory hook installed`);
|
|
684
|
+
}
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.log(`\n ${red("x")} Hook install failed: ${e.message}`);
|
|
687
|
+
}
|
|
688
|
+
} else if (!isNonInteractive && !hookFlag) {
|
|
689
|
+
console.log(
|
|
690
|
+
dim(` Skipped — install later: context-vault hooks install`),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
618
695
|
// Seed entry
|
|
619
696
|
const seeded = createSeedEntries(resolvedVaultDir);
|
|
620
697
|
if (seeded > 0) {
|
|
@@ -624,7 +701,7 @@ async function runSetup() {
|
|
|
624
701
|
}
|
|
625
702
|
|
|
626
703
|
// Health check
|
|
627
|
-
console.log(`\n ${dim("[
|
|
704
|
+
console.log(`\n ${dim("[6/6]")}${bold(" Health check...")}\n`);
|
|
628
705
|
const okResults = results.filter((r) => r.ok);
|
|
629
706
|
|
|
630
707
|
// Verify DB is accessible
|
|
@@ -1308,6 +1385,69 @@ async function runReindex() {
|
|
|
1308
1385
|
console.log(` ${dim("·")} ${stats.unchanged} unchanged`);
|
|
1309
1386
|
}
|
|
1310
1387
|
|
|
1388
|
+
async function runPrune() {
|
|
1389
|
+
const dryRun = flags.has("--dry-run");
|
|
1390
|
+
|
|
1391
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1392
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
1393
|
+
await import("@context-vault/core/index/db");
|
|
1394
|
+
const { pruneExpired } = await import("@context-vault/core/index");
|
|
1395
|
+
|
|
1396
|
+
const config = resolveConfig();
|
|
1397
|
+
if (!config.vaultDirExists) {
|
|
1398
|
+
console.error(red(`Vault directory not found: ${config.vaultDir}`));
|
|
1399
|
+
console.error("Run " + cyan("context-vault setup") + " to configure.");
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const db = await initDatabase(config.dbPath);
|
|
1404
|
+
|
|
1405
|
+
if (dryRun) {
|
|
1406
|
+
const expired = db
|
|
1407
|
+
.prepare(
|
|
1408
|
+
"SELECT id, kind, title, expires_at FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
|
|
1409
|
+
)
|
|
1410
|
+
.all();
|
|
1411
|
+
db.close();
|
|
1412
|
+
|
|
1413
|
+
if (expired.length === 0) {
|
|
1414
|
+
console.log(green(" No expired entries found."));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
console.log(
|
|
1419
|
+
`\n ${bold(String(expired.length))} expired ${expired.length === 1 ? "entry" : "entries"} would be removed:\n`,
|
|
1420
|
+
);
|
|
1421
|
+
for (const e of expired) {
|
|
1422
|
+
const label = e.title ? `${e.kind}: ${e.title}` : `${e.kind} (${e.id})`;
|
|
1423
|
+
console.log(` ${dim("-")} ${label} ${dim(`(expired ${e.expires_at})`)}`);
|
|
1424
|
+
}
|
|
1425
|
+
console.log(dim("\n Dry run — no entries were removed."));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const stmts = prepareStatements(db);
|
|
1430
|
+
const ctx = {
|
|
1431
|
+
db,
|
|
1432
|
+
config,
|
|
1433
|
+
stmts,
|
|
1434
|
+
embed: async () => null,
|
|
1435
|
+
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
1436
|
+
deleteVec: (r) => deleteVec(stmts, r),
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
const count = await pruneExpired(ctx);
|
|
1440
|
+
db.close();
|
|
1441
|
+
|
|
1442
|
+
if (count === 0) {
|
|
1443
|
+
console.log(green(" No expired entries found."));
|
|
1444
|
+
} else {
|
|
1445
|
+
console.log(
|
|
1446
|
+
green(` ✓ Pruned ${count} expired ${count === 1 ? "entry" : "entries"}`),
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1311
1451
|
async function runStatus() {
|
|
1312
1452
|
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1313
1453
|
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
@@ -1900,6 +2040,196 @@ async function runIngest() {
|
|
|
1900
2040
|
console.log();
|
|
1901
2041
|
}
|
|
1902
2042
|
|
|
2043
|
+
async function runRecall() {
|
|
2044
|
+
let query;
|
|
2045
|
+
|
|
2046
|
+
if (!process.stdin.isTTY) {
|
|
2047
|
+
const raw = await new Promise((resolve) => {
|
|
2048
|
+
let data = "";
|
|
2049
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
2050
|
+
process.stdin.on("end", () => resolve(data));
|
|
2051
|
+
});
|
|
2052
|
+
try {
|
|
2053
|
+
const payload = JSON.parse(raw);
|
|
2054
|
+
query = payload.prompt || payload.query || "";
|
|
2055
|
+
} catch {
|
|
2056
|
+
query = args[1] || raw.trim();
|
|
2057
|
+
}
|
|
2058
|
+
} else {
|
|
2059
|
+
query = args.slice(1).join(" ");
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
if (!query?.trim()) return;
|
|
2063
|
+
|
|
2064
|
+
let db;
|
|
2065
|
+
try {
|
|
2066
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2067
|
+
const config = resolveConfig();
|
|
2068
|
+
|
|
2069
|
+
if (!config.vaultDirExists) return;
|
|
2070
|
+
|
|
2071
|
+
const { initDatabase, prepareStatements } =
|
|
2072
|
+
await import("@context-vault/core/index/db");
|
|
2073
|
+
const { embed } = await import("@context-vault/core/index/embed");
|
|
2074
|
+
const { hybridSearch } = await import("@context-vault/core/retrieve/index");
|
|
2075
|
+
|
|
2076
|
+
db = await initDatabase(config.dbPath);
|
|
2077
|
+
const stmts = prepareStatements(db);
|
|
2078
|
+
const ctx = { db, config, stmts, embed };
|
|
2079
|
+
|
|
2080
|
+
const results = await hybridSearch(ctx, query, { limit: 5 });
|
|
2081
|
+
if (!results.length) return;
|
|
2082
|
+
|
|
2083
|
+
const lines = ["## Context Vault\n"];
|
|
2084
|
+
for (const r of results) {
|
|
2085
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
2086
|
+
lines.push(`### ${r.title || "(untitled)"} [${r.kind}]`);
|
|
2087
|
+
if (entryTags.length) lines.push(`tags: ${entryTags.join(", ")}`);
|
|
2088
|
+
lines.push(r.body?.slice(0, 400) + (r.body?.length > 400 ? "..." : ""));
|
|
2089
|
+
lines.push("");
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
process.stdout.write(lines.join("\n"));
|
|
2093
|
+
} catch {
|
|
2094
|
+
// fail silently — never interrupt the user's workflow
|
|
2095
|
+
} finally {
|
|
2096
|
+
try {
|
|
2097
|
+
db?.close();
|
|
2098
|
+
} catch {}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/** Returns the path to Claude Code's global settings.json */
|
|
2103
|
+
function claudeSettingsPath() {
|
|
2104
|
+
return join(HOME, ".claude", "settings.json");
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* Writes a UserPromptSubmit hook entry for context-vault recall to ~/.claude/settings.json.
|
|
2109
|
+
* Returns true if installed, false if already present.
|
|
2110
|
+
*/
|
|
2111
|
+
function installClaudeHook() {
|
|
2112
|
+
const settingsPath = claudeSettingsPath();
|
|
2113
|
+
let settings = {};
|
|
2114
|
+
|
|
2115
|
+
if (existsSync(settingsPath)) {
|
|
2116
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
2117
|
+
try {
|
|
2118
|
+
settings = JSON.parse(raw);
|
|
2119
|
+
} catch {
|
|
2120
|
+
const bak = settingsPath + ".bak";
|
|
2121
|
+
copyFileSync(settingsPath, bak);
|
|
2122
|
+
console.log(yellow(` Backed up corrupted settings to ${bak}`));
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2127
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
2128
|
+
|
|
2129
|
+
const alreadyInstalled = settings.hooks.UserPromptSubmit.some((h) =>
|
|
2130
|
+
h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
|
|
2131
|
+
);
|
|
2132
|
+
if (alreadyInstalled) return false;
|
|
2133
|
+
|
|
2134
|
+
settings.hooks.UserPromptSubmit.push({
|
|
2135
|
+
hooks: [
|
|
2136
|
+
{
|
|
2137
|
+
type: "command",
|
|
2138
|
+
command: "context-vault recall",
|
|
2139
|
+
timeout: 10,
|
|
2140
|
+
},
|
|
2141
|
+
],
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
2145
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Removes the context-vault recall hook from ~/.claude/settings.json.
|
|
2151
|
+
* Returns true if removed, false if not found.
|
|
2152
|
+
*/
|
|
2153
|
+
function removeClaudeHook() {
|
|
2154
|
+
const settingsPath = claudeSettingsPath();
|
|
2155
|
+
if (!existsSync(settingsPath)) return false;
|
|
2156
|
+
|
|
2157
|
+
let settings;
|
|
2158
|
+
try {
|
|
2159
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
2160
|
+
} catch {
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (!settings.hooks?.UserPromptSubmit) return false;
|
|
2165
|
+
|
|
2166
|
+
const before = settings.hooks.UserPromptSubmit.length;
|
|
2167
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
2168
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
|
|
2169
|
+
);
|
|
2170
|
+
|
|
2171
|
+
if (settings.hooks.UserPromptSubmit.length === before) return false;
|
|
2172
|
+
|
|
2173
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
async function runHooks() {
|
|
2178
|
+
const sub = args[1];
|
|
2179
|
+
|
|
2180
|
+
if (sub === "install") {
|
|
2181
|
+
try {
|
|
2182
|
+
const installed = installClaudeHook();
|
|
2183
|
+
if (installed) {
|
|
2184
|
+
console.log(`\n ${green("✓")} Claude Code memory hook installed.\n`);
|
|
2185
|
+
console.log(
|
|
2186
|
+
dim(
|
|
2187
|
+
" On every prompt, context-vault searches your vault for relevant entries",
|
|
2188
|
+
),
|
|
2189
|
+
);
|
|
2190
|
+
console.log(
|
|
2191
|
+
dim(
|
|
2192
|
+
" and injects them as additional context alongside Claude's native memory.",
|
|
2193
|
+
),
|
|
2194
|
+
);
|
|
2195
|
+
console.log(
|
|
2196
|
+
dim(`\n To remove: ${cyan("context-vault hooks remove")}`),
|
|
2197
|
+
);
|
|
2198
|
+
} else {
|
|
2199
|
+
console.log(`\n ${yellow("!")} Hook already installed.\n`);
|
|
2200
|
+
}
|
|
2201
|
+
} catch (e) {
|
|
2202
|
+
console.error(`\n ${red("x")} Failed to install hook: ${e.message}\n`);
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
console.log();
|
|
2206
|
+
} else if (sub === "remove") {
|
|
2207
|
+
try {
|
|
2208
|
+
const removed = removeClaudeHook();
|
|
2209
|
+
if (removed) {
|
|
2210
|
+
console.log(`\n ${green("✓")} Claude Code memory hook removed.\n`);
|
|
2211
|
+
} else {
|
|
2212
|
+
console.log(`\n ${yellow("!")} Hook not found — nothing to remove.\n`);
|
|
2213
|
+
}
|
|
2214
|
+
} catch (e) {
|
|
2215
|
+
console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
|
|
2216
|
+
process.exit(1);
|
|
2217
|
+
}
|
|
2218
|
+
} else {
|
|
2219
|
+
console.log(`
|
|
2220
|
+
${bold("context-vault hooks")} <install|remove>
|
|
2221
|
+
|
|
2222
|
+
Manage the Claude Code memory hook integration.
|
|
2223
|
+
When installed, context-vault automatically searches your vault on every user
|
|
2224
|
+
prompt and injects relevant entries as additional context.
|
|
2225
|
+
|
|
2226
|
+
${bold("Commands:")}
|
|
2227
|
+
${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2228
|
+
${cyan("hooks remove")} Remove the hook from ~/.claude/settings.json
|
|
2229
|
+
`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
1903
2233
|
async function runServe() {
|
|
1904
2234
|
await import("../src/server/index.js");
|
|
1905
2235
|
}
|
|
@@ -1928,6 +2258,12 @@ async function main() {
|
|
|
1928
2258
|
case "serve":
|
|
1929
2259
|
await runServe();
|
|
1930
2260
|
break;
|
|
2261
|
+
case "hooks":
|
|
2262
|
+
await runHooks();
|
|
2263
|
+
break;
|
|
2264
|
+
case "recall":
|
|
2265
|
+
await runRecall();
|
|
2266
|
+
break;
|
|
1931
2267
|
case "import":
|
|
1932
2268
|
await runImport();
|
|
1933
2269
|
break;
|
|
@@ -1940,6 +2276,9 @@ async function main() {
|
|
|
1940
2276
|
case "reindex":
|
|
1941
2277
|
await runReindex();
|
|
1942
2278
|
break;
|
|
2279
|
+
case "prune":
|
|
2280
|
+
await runPrune();
|
|
2281
|
+
break;
|
|
1943
2282
|
case "status":
|
|
1944
2283
|
await runStatus();
|
|
1945
2284
|
break;
|
|
@@ -32,10 +32,12 @@ export function writeEntryFile(
|
|
|
32
32
|
tags,
|
|
33
33
|
source,
|
|
34
34
|
createdAt,
|
|
35
|
+
updatedAt,
|
|
35
36
|
folder,
|
|
36
37
|
category,
|
|
37
38
|
identity_key,
|
|
38
39
|
expires_at,
|
|
40
|
+
supersedes,
|
|
39
41
|
},
|
|
40
42
|
) {
|
|
41
43
|
// P5: folder is now a top-level param; also accept from meta for backward compat
|
|
@@ -61,9 +63,11 @@ export function writeEntryFile(
|
|
|
61
63
|
|
|
62
64
|
if (identity_key) fmFields.identity_key = identity_key;
|
|
63
65
|
if (expires_at) fmFields.expires_at = expires_at;
|
|
66
|
+
if (supersedes?.length) fmFields.supersedes = supersedes;
|
|
64
67
|
fmFields.tags = tags || [];
|
|
65
68
|
fmFields.source = source || "claude-code";
|
|
66
69
|
fmFields.created = created;
|
|
70
|
+
if (updatedAt && updatedAt !== created) fmFields.updated = updatedAt;
|
|
67
71
|
|
|
68
72
|
const mdBody = formatBody(kind, { title, body, meta });
|
|
69
73
|
|
|
@@ -26,6 +26,7 @@ export function writeEntry(
|
|
|
26
26
|
folder,
|
|
27
27
|
identity_key,
|
|
28
28
|
expires_at,
|
|
29
|
+
supersedes,
|
|
29
30
|
userId,
|
|
30
31
|
},
|
|
31
32
|
) {
|
|
@@ -47,6 +48,7 @@ export function writeEntry(
|
|
|
47
48
|
// Entity upsert: check for existing file at deterministic path
|
|
48
49
|
let id;
|
|
49
50
|
let createdAt;
|
|
51
|
+
let updatedAt;
|
|
50
52
|
if (category === "entity" && identity_key) {
|
|
51
53
|
const identitySlug = slugify(identity_key);
|
|
52
54
|
const dir = resolve(ctx.config.vaultDir, kindToPath(kind));
|
|
@@ -58,13 +60,16 @@ export function writeEntry(
|
|
|
58
60
|
const { meta: fmMeta } = parseFrontmatter(raw);
|
|
59
61
|
id = fmMeta.id || ulid();
|
|
60
62
|
createdAt = fmMeta.created || new Date().toISOString();
|
|
63
|
+
updatedAt = new Date().toISOString();
|
|
61
64
|
} else {
|
|
62
65
|
id = ulid();
|
|
63
66
|
createdAt = new Date().toISOString();
|
|
67
|
+
updatedAt = createdAt;
|
|
64
68
|
}
|
|
65
69
|
} else {
|
|
66
70
|
id = ulid();
|
|
67
71
|
createdAt = new Date().toISOString();
|
|
72
|
+
updatedAt = createdAt;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
const filePath = writeEntryFile(ctx.config.vaultDir, kind, {
|
|
@@ -75,10 +80,12 @@ export function writeEntry(
|
|
|
75
80
|
tags,
|
|
76
81
|
source,
|
|
77
82
|
createdAt,
|
|
83
|
+
updatedAt,
|
|
78
84
|
folder,
|
|
79
85
|
category,
|
|
80
86
|
identity_key,
|
|
81
87
|
expires_at,
|
|
88
|
+
supersedes,
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
return {
|
|
@@ -92,8 +99,10 @@ export function writeEntry(
|
|
|
92
99
|
tags,
|
|
93
100
|
source,
|
|
94
101
|
createdAt,
|
|
102
|
+
updatedAt,
|
|
95
103
|
identity_key,
|
|
96
104
|
expires_at,
|
|
105
|
+
supersedes,
|
|
97
106
|
userId: userId || null,
|
|
98
107
|
};
|
|
99
108
|
}
|
|
@@ -121,6 +130,10 @@ export function updateEntryFile(ctx, existing, updates) {
|
|
|
121
130
|
updates.source !== undefined ? updates.source : existing.source;
|
|
122
131
|
const expires_at =
|
|
123
132
|
updates.expires_at !== undefined ? updates.expires_at : existing.expires_at;
|
|
133
|
+
const supersedes =
|
|
134
|
+
updates.supersedes !== undefined
|
|
135
|
+
? updates.supersedes
|
|
136
|
+
: fmMeta.supersedes || null;
|
|
124
137
|
|
|
125
138
|
let mergedMeta;
|
|
126
139
|
if (updates.meta !== undefined) {
|
|
@@ -130,6 +143,7 @@ export function updateEntryFile(ctx, existing, updates) {
|
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// Build frontmatter
|
|
146
|
+
const now = new Date().toISOString();
|
|
133
147
|
const fmFields = { id: existing.id };
|
|
134
148
|
for (const [k, v] of Object.entries(mergedMeta)) {
|
|
135
149
|
if (k === "folder") continue;
|
|
@@ -137,9 +151,11 @@ export function updateEntryFile(ctx, existing, updates) {
|
|
|
137
151
|
}
|
|
138
152
|
if (existing.identity_key) fmFields.identity_key = existing.identity_key;
|
|
139
153
|
if (expires_at) fmFields.expires_at = expires_at;
|
|
154
|
+
if (supersedes?.length) fmFields.supersedes = supersedes;
|
|
140
155
|
fmFields.tags = tags;
|
|
141
156
|
fmFields.source = source || "claude-code";
|
|
142
157
|
fmFields.created = fmMeta.created || existing.created_at;
|
|
158
|
+
if (now !== fmFields.created) fmFields.updated = now;
|
|
143
159
|
|
|
144
160
|
const mdBody = formatBody(existing.kind, { title, body, meta: mergedMeta });
|
|
145
161
|
const md = formatFrontmatter(fmFields) + mdBody;
|
|
@@ -159,8 +175,10 @@ export function updateEntryFile(ctx, existing, updates) {
|
|
|
159
175
|
tags,
|
|
160
176
|
source,
|
|
161
177
|
createdAt: fmMeta.created || existing.created_at,
|
|
178
|
+
updatedAt: now,
|
|
162
179
|
identity_key: existing.identity_key,
|
|
163
180
|
expires_at,
|
|
181
|
+
supersedes,
|
|
164
182
|
userId: existing.user_id || null,
|
|
165
183
|
};
|
|
166
184
|
}
|
|
@@ -180,6 +198,14 @@ export async function captureAndIndex(ctx, data) {
|
|
|
180
198
|
const entry = writeEntry(ctx, data);
|
|
181
199
|
try {
|
|
182
200
|
await indexEntry(ctx, entry);
|
|
201
|
+
// Apply supersedes: mark referenced entries as superseded by this entry
|
|
202
|
+
if (entry.supersedes?.length && ctx.stmts.updateSupersededBy) {
|
|
203
|
+
for (const supersededId of entry.supersedes) {
|
|
204
|
+
if (typeof supersededId === "string" && supersededId.trim()) {
|
|
205
|
+
ctx.stmts.updateSupersededBy.run(entry.id, supersededId.trim());
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
183
209
|
return entry;
|
|
184
210
|
} catch (err) {
|
|
185
211
|
// Rollback: restore previous content for entity upserts, delete for new entries
|
|
@@ -6,3 +6,10 @@ export const MAX_TAGS_COUNT = 20;
|
|
|
6
6
|
export const MAX_META_LENGTH = 10 * 1024; // 10KB
|
|
7
7
|
export const MAX_SOURCE_LENGTH = 200;
|
|
8
8
|
export const MAX_IDENTITY_KEY_LENGTH = 200;
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_GROWTH_THRESHOLDS = {
|
|
11
|
+
totalEntries: { warn: 1000, critical: 5000 },
|
|
12
|
+
eventEntries: { warn: 500, critical: 2000 },
|
|
13
|
+
vaultSizeBytes: { warn: 50 * 1024 * 1024, critical: 200 * 1024 * 1024 },
|
|
14
|
+
eventsWithoutTtl: { warn: 200 },
|
|
15
|
+
};
|
|
@@ -40,6 +40,17 @@ const CATEGORY_DIR_NAMES = {
|
|
|
40
40
|
/** Set of valid category directory names (for reindex discovery) */
|
|
41
41
|
export const CATEGORY_DIRS = new Set(Object.values(CATEGORY_DIR_NAMES));
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Staleness thresholds (in days) per knowledge kind.
|
|
45
|
+
* Kinds not listed here are considered enduring (no staleness threshold).
|
|
46
|
+
* Based on updated_at; falls back to created_at if updated_at is null.
|
|
47
|
+
*/
|
|
48
|
+
export const KIND_STALENESS_DAYS = {
|
|
49
|
+
pattern: 180,
|
|
50
|
+
decision: 365,
|
|
51
|
+
reference: 90,
|
|
52
|
+
};
|
|
53
|
+
|
|
43
54
|
export function categoryFor(kind) {
|
|
44
55
|
return KIND_CATEGORY[kind] || "knowledge";
|
|
45
56
|
}
|
|
@@ -1,6 +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
5
|
|
|
5
6
|
export function parseArgs(argv) {
|
|
6
7
|
const args = {};
|
|
@@ -31,6 +32,8 @@ export function resolveConfig() {
|
|
|
31
32
|
dbPath: join(dataDir, "vault.db"),
|
|
32
33
|
devDir: join(HOME, "dev"),
|
|
33
34
|
eventDecayDays: 30,
|
|
35
|
+
thresholds: { ...DEFAULT_GROWTH_THRESHOLDS },
|
|
36
|
+
telemetry: false,
|
|
34
37
|
resolvedFrom: "defaults",
|
|
35
38
|
};
|
|
36
39
|
|
|
@@ -46,6 +49,30 @@ export function resolveConfig() {
|
|
|
46
49
|
if (fc.dbPath) config.dbPath = fc.dbPath;
|
|
47
50
|
if (fc.devDir) config.devDir = fc.devDir;
|
|
48
51
|
if (fc.eventDecayDays != null) config.eventDecayDays = fc.eventDecayDays;
|
|
52
|
+
if (fc.thresholds) {
|
|
53
|
+
const t = fc.thresholds;
|
|
54
|
+
if (t.totalEntries)
|
|
55
|
+
config.thresholds.totalEntries = {
|
|
56
|
+
...config.thresholds.totalEntries,
|
|
57
|
+
...t.totalEntries,
|
|
58
|
+
};
|
|
59
|
+
if (t.eventEntries)
|
|
60
|
+
config.thresholds.eventEntries = {
|
|
61
|
+
...config.thresholds.eventEntries,
|
|
62
|
+
...t.eventEntries,
|
|
63
|
+
};
|
|
64
|
+
if (t.vaultSizeBytes)
|
|
65
|
+
config.thresholds.vaultSizeBytes = {
|
|
66
|
+
...config.thresholds.vaultSizeBytes,
|
|
67
|
+
...t.vaultSizeBytes,
|
|
68
|
+
};
|
|
69
|
+
if (t.eventsWithoutTtl)
|
|
70
|
+
config.thresholds.eventsWithoutTtl = {
|
|
71
|
+
...config.thresholds.eventsWithoutTtl,
|
|
72
|
+
...t.eventsWithoutTtl,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (fc.telemetry != null) config.telemetry = fc.telemetry === true;
|
|
49
76
|
// Hosted account linking (Phase 4)
|
|
50
77
|
if (fc.hostedUrl) config.hostedUrl = fc.hostedUrl;
|
|
51
78
|
if (fc.apiKey) config.apiKey = fc.apiKey;
|
|
@@ -96,6 +123,11 @@ export function resolveConfig() {
|
|
|
96
123
|
if (process.env.CONTEXT_VAULT_HOSTED_URL) {
|
|
97
124
|
config.hostedUrl = process.env.CONTEXT_VAULT_HOSTED_URL;
|
|
98
125
|
}
|
|
126
|
+
if (process.env.CONTEXT_VAULT_TELEMETRY !== undefined) {
|
|
127
|
+
config.telemetry =
|
|
128
|
+
process.env.CONTEXT_VAULT_TELEMETRY === "1" ||
|
|
129
|
+
process.env.CONTEXT_VAULT_TELEMETRY === "true";
|
|
130
|
+
}
|
|
99
131
|
|
|
100
132
|
if (cliArgs.vaultDir) {
|
|
101
133
|
config.vaultDir = cliArgs.vaultDir;
|