context-vault 2.10.3 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -25,6 +25,7 @@ import { join, resolve, dirname } from "node:path";
25
25
  import { homedir, platform } from "node:os";
26
26
  import { execSync, execFile, execFileSync } from "node:child_process";
27
27
  import { fileURLToPath } from "node:url";
28
+ import { APP_URL, API_URL, MARKETING_URL } from "@context-vault/core/constants";
28
29
 
29
30
  const __filename = fileURLToPath(import.meta.url);
30
31
  const __dirname = dirname(__filename);
@@ -234,7 +235,10 @@ ${bold("Commands:")}
234
235
  ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
235
236
  ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
236
237
  ${cyan("serve")} Start the MCP server (used by AI clients)
238
+ ${cyan("hooks")} install|remove Install or remove Claude Code memory hook
239
+ ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
237
240
  ${cyan("reindex")} Rebuild search index from knowledge files
241
+ ${cyan("prune")} Remove expired entries (use --dry-run to preview)
238
242
  ${cyan("status")} Show vault diagnostics
239
243
  ${cyan("update")} Check for and install updates
240
244
  ${cyan("uninstall")} Remove MCP configs and optionally data
@@ -269,9 +273,39 @@ async function runSetup() {
269
273
  existingVault = cfg.vaultDir || existingVault;
270
274
  } catch {}
271
275
 
276
+ // Version check against npm registry (5s timeout, fail silently if offline)
277
+ let latestVersion = null;
278
+ try {
279
+ latestVersion = execSync("npm view context-vault version", {
280
+ encoding: "utf-8",
281
+ stdio: ["pipe", "pipe", "pipe"],
282
+ timeout: 5000,
283
+ }).trim();
284
+ } catch {}
285
+
286
+ if (latestVersion === VERSION) {
287
+ console.log(
288
+ green(` ✓ context-vault v${VERSION} is up to date`) +
289
+ dim(` (vault: ${existingVault})`),
290
+ );
291
+ console.log();
292
+ return;
293
+ }
294
+
272
295
  console.log(yellow(` Existing installation detected`));
273
296
  console.log(dim(` Vault: ${existingVault}`));
274
- console.log(dim(` Config: ${existingConfig}`));
297
+ if (latestVersion) {
298
+ console.log();
299
+ console.log(` Current: ${dim(VERSION)}`);
300
+ console.log(` Latest: ${green(latestVersion)}`);
301
+ const upgradeCmd = isNpx()
302
+ ? "npx context-vault@latest setup"
303
+ : "npm install -g context-vault";
304
+ console.log();
305
+ console.log(dim(` To upgrade: ${upgradeCmd}`));
306
+ } else {
307
+ console.log(dim(` Config: ${existingConfig}`));
308
+ }
275
309
  console.log();
276
310
  console.log(` 1) Full reconfigure`);
277
311
  console.log(` 2) Update tool configs only ${dim("(skip vault setup)")}`);
@@ -351,6 +385,7 @@ async function runSetup() {
351
385
 
352
386
  console.log();
353
387
  console.log(green(" ✓ Tool configs updated."));
388
+ console.log(dim(" Restart your AI tools to apply the changes."));
354
389
  console.log();
355
390
  return;
356
391
  }
@@ -359,7 +394,7 @@ async function runSetup() {
359
394
  }
360
395
 
361
396
  // Detect tools
362
- console.log(dim(` [1/5]`) + bold(" Detecting tools...\n"));
397
+ console.log(dim(` [1/6]`) + bold(" Detecting tools...\n"));
363
398
  const { detected, results: detectionResults } = await detectAllTools();
364
399
  printDetectionResults(detectionResults);
365
400
  console.log();
@@ -427,7 +462,7 @@ async function runSetup() {
427
462
  }
428
463
 
429
464
  // Vault directory (content files)
430
- console.log(dim(` [2/5]`) + bold(" Configuring vault...\n"));
465
+ console.log(dim(` [2/6]`) + bold(" Configuring vault...\n"));
431
466
  const defaultVaultDir = getFlag("--vault-dir") || join(HOME, "vault");
432
467
  const vaultDir = isNonInteractive
433
468
  ? defaultVaultDir
@@ -484,6 +519,39 @@ async function runSetup() {
484
519
  vaultConfig.dbPath = join(dataDir, "vault.db");
485
520
  vaultConfig.devDir = join(HOME, "dev");
486
521
  vaultConfig.mode = "local";
522
+
523
+ // Telemetry opt-in
524
+ console.log(`\n ${dim("[3/6]")}${bold(" Anonymous error reporting\n")}`);
525
+ console.log(
526
+ dim(
527
+ " When enabled, unhandled errors send a minimal event (type, tool name,",
528
+ ),
529
+ );
530
+ console.log(
531
+ dim(" version, platform) to help diagnose issues. No vault content,"),
532
+ );
533
+ console.log(
534
+ dim(" file paths, or personal data is ever sent. Off by default."),
535
+ );
536
+ console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
537
+ console.log();
538
+
539
+ let telemetryEnabled = vaultConfig.telemetry === true;
540
+ if (!isNonInteractive) {
541
+ const defaultChoice = telemetryEnabled ? "Y" : "n";
542
+ const telemetryAnswer = await prompt(
543
+ ` Enable anonymous error reporting? (y/N):`,
544
+ defaultChoice,
545
+ );
546
+ telemetryEnabled =
547
+ telemetryAnswer.toLowerCase() === "y" ||
548
+ telemetryAnswer.toLowerCase() === "yes";
549
+ }
550
+ vaultConfig.telemetry = telemetryEnabled;
551
+ console.log(
552
+ ` ${telemetryEnabled ? green("+") : dim("-")} Telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`,
553
+ );
554
+
487
555
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
488
556
  console.log(`\n ${green("+")} Wrote ${configPath}`);
489
557
 
@@ -491,7 +559,7 @@ async function runSetup() {
491
559
  const skipEmbeddings = flags.has("--skip-embeddings");
492
560
  if (skipEmbeddings) {
493
561
  console.log(
494
- `\n ${dim("[3/5]")}${bold(" Embedding model")} ${dim("(skipped)")}`,
562
+ `\n ${dim("[4/6]")}${bold(" Embedding model")} ${dim("(skipped)")}`,
495
563
  );
496
564
  console.log(
497
565
  dim(
@@ -503,9 +571,14 @@ async function runSetup() {
503
571
  );
504
572
  } else {
505
573
  console.log(
506
- `\n ${dim("[3/5]")}${bold(" Downloading embedding model...")}`,
574
+ `\n ${dim("[4/6]")}${bold(" Downloading embedding model...")}`,
575
+ );
576
+ console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)"));
577
+ console.log(
578
+ dim(
579
+ ` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`,
580
+ ),
507
581
  );
508
- console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
509
582
  {
510
583
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
511
584
  let frame = 0;
@@ -592,7 +665,7 @@ async function runSetup() {
592
665
  }
593
666
 
594
667
  // Configure each tool — pass vault dir as arg if non-default
595
- console.log(`\n ${dim("[4/5]")}${bold(" Configuring tools...\n")}`);
668
+ console.log(`\n ${dim("[5/6]")}${bold(" Configuring tools...\n")}`);
596
669
  const results = [];
597
670
  const defaultVDir = join(HOME, "vault");
598
671
  const customVaultDir =
@@ -615,6 +688,47 @@ async function runSetup() {
615
688
  }
616
689
  }
617
690
 
691
+ // Claude Code memory hook (opt-in)
692
+ const claudeConfigured = results.some(
693
+ (r) => r.ok && r.tool.id === "claude-code",
694
+ );
695
+ const hookFlag = flags.has("--hooks");
696
+ if (claudeConfigured) {
697
+ let installHook = hookFlag;
698
+ if (!hookFlag && !isNonInteractive) {
699
+ console.log();
700
+ console.log(dim(" Claude Code detected — install memory hook?"));
701
+ console.log(
702
+ dim(
703
+ " Searches your vault on every prompt and injects relevant entries",
704
+ ),
705
+ );
706
+ console.log(
707
+ dim(" as additional context alongside Claude's native memory."),
708
+ );
709
+ console.log();
710
+ const answer = await prompt(
711
+ " Install Claude Code memory hook? (y/N):",
712
+ "N",
713
+ );
714
+ installHook = answer.toLowerCase() === "y";
715
+ }
716
+ if (installHook) {
717
+ try {
718
+ const installed = installClaudeHook();
719
+ if (installed) {
720
+ console.log(`\n ${green("+")} Memory hook installed`);
721
+ }
722
+ } catch (e) {
723
+ console.log(`\n ${red("x")} Hook install failed: ${e.message}`);
724
+ }
725
+ } else if (!isNonInteractive && !hookFlag) {
726
+ console.log(
727
+ dim(` Skipped — install later: context-vault hooks install`),
728
+ );
729
+ }
730
+ }
731
+
618
732
  // Seed entry
619
733
  const seeded = createSeedEntries(resolvedVaultDir);
620
734
  if (seeded > 0) {
@@ -624,7 +738,7 @@ async function runSetup() {
624
738
  }
625
739
 
626
740
  // Health check
627
- console.log(`\n ${dim("[5/5]")}${bold(" Health check...")}\n`);
741
+ console.log(`\n ${dim("[6/6]")}${bold(" Health check...")}\n`);
628
742
  const okResults = results.filter((r) => r.ok);
629
743
 
630
744
  // Verify DB is accessible
@@ -654,7 +768,9 @@ async function runSetup() {
654
768
  const boxLines = [
655
769
  ` ✓ Setup complete — ${passed}/${checks.length} checks passed (${elapsed}s)`,
656
770
  ``,
657
- ` ${bold("AI Tools")} open ${toolName} and try:`,
771
+ ` ${bold("Next:")} restart ${toolName} to activate the vault`,
772
+ ``,
773
+ ` ${bold("AI Tools")} — once active, try:`,
658
774
  ` "Search my vault for getting started"`,
659
775
  ` "Save an insight about [topic]"`,
660
776
  ` "Show my vault status"`,
@@ -902,7 +1018,7 @@ This is an example entry showing the decision format. Feel free to delete it.
902
1018
 
903
1019
  async function runConnect() {
904
1020
  const apiKey = getFlag("--key");
905
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1021
+ const hostedUrl = getFlag("--url") || API_URL;
906
1022
 
907
1023
  if (!apiKey) {
908
1024
  console.log(`\n ${bold("context-vault connect")}\n`);
@@ -911,9 +1027,7 @@ async function runConnect() {
911
1027
  console.log(` context-vault connect --key cv_...\n`);
912
1028
  console.log(` Options:`);
913
1029
  console.log(` --key <key> API key (required)`);
914
- console.log(
915
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
916
- );
1030
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
917
1031
  console.log();
918
1032
  return;
919
1033
  }
@@ -1155,9 +1269,7 @@ async function runSwitch() {
1155
1269
  );
1156
1270
  console.log(` Options:`);
1157
1271
  console.log(` --key <key> API key for hosted mode (cv_...)`);
1158
- console.log(
1159
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)\n`,
1160
- );
1272
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})\n`);
1161
1273
  return;
1162
1274
  }
1163
1275
 
@@ -1214,10 +1326,7 @@ async function runSwitch() {
1214
1326
  console.log(dim(` Server: node ${launcherPath}`));
1215
1327
  console.log();
1216
1328
  } else {
1217
- const hostedUrl =
1218
- getFlag("--url") ||
1219
- vaultConfig.hostedUrl ||
1220
- "https://api.context-vault.com";
1329
+ const hostedUrl = getFlag("--url") || vaultConfig.hostedUrl || API_URL;
1221
1330
  const apiKey = getFlag("--key") || vaultConfig.apiKey;
1222
1331
 
1223
1332
  if (!apiKey) {
@@ -1308,6 +1417,69 @@ async function runReindex() {
1308
1417
  console.log(` ${dim("·")} ${stats.unchanged} unchanged`);
1309
1418
  }
1310
1419
 
1420
+ async function runPrune() {
1421
+ const dryRun = flags.has("--dry-run");
1422
+
1423
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1424
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
1425
+ await import("@context-vault/core/index/db");
1426
+ const { pruneExpired } = await import("@context-vault/core/index");
1427
+
1428
+ const config = resolveConfig();
1429
+ if (!config.vaultDirExists) {
1430
+ console.error(red(`Vault directory not found: ${config.vaultDir}`));
1431
+ console.error("Run " + cyan("context-vault setup") + " to configure.");
1432
+ process.exit(1);
1433
+ }
1434
+
1435
+ const db = await initDatabase(config.dbPath);
1436
+
1437
+ if (dryRun) {
1438
+ const expired = db
1439
+ .prepare(
1440
+ "SELECT id, kind, title, expires_at FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
1441
+ )
1442
+ .all();
1443
+ db.close();
1444
+
1445
+ if (expired.length === 0) {
1446
+ console.log(green(" No expired entries found."));
1447
+ return;
1448
+ }
1449
+
1450
+ console.log(
1451
+ `\n ${bold(String(expired.length))} expired ${expired.length === 1 ? "entry" : "entries"} would be removed:\n`,
1452
+ );
1453
+ for (const e of expired) {
1454
+ const label = e.title ? `${e.kind}: ${e.title}` : `${e.kind} (${e.id})`;
1455
+ console.log(` ${dim("-")} ${label} ${dim(`(expired ${e.expires_at})`)}`);
1456
+ }
1457
+ console.log(dim("\n Dry run — no entries were removed."));
1458
+ return;
1459
+ }
1460
+
1461
+ const stmts = prepareStatements(db);
1462
+ const ctx = {
1463
+ db,
1464
+ config,
1465
+ stmts,
1466
+ embed: async () => null,
1467
+ insertVec: (r, e) => insertVec(stmts, r, e),
1468
+ deleteVec: (r) => deleteVec(stmts, r),
1469
+ };
1470
+
1471
+ const count = await pruneExpired(ctx);
1472
+ db.close();
1473
+
1474
+ if (count === 0) {
1475
+ console.log(green(" No expired entries found."));
1476
+ } else {
1477
+ console.log(
1478
+ green(` ✓ Pruned ${count} expired ${count === 1 ? "entry" : "entries"}`),
1479
+ );
1480
+ }
1481
+ }
1482
+
1311
1483
  async function runStatus() {
1312
1484
  const { resolveConfig } = await import("@context-vault/core/core/config");
1313
1485
  const { initDatabase } = await import("@context-vault/core/index/db");
@@ -1534,15 +1706,13 @@ async function runMigrate() {
1534
1706
  ` context-vault migrate --to-local Download hosted vault to local files`,
1535
1707
  );
1536
1708
  console.log(`\n Options:`);
1537
- console.log(
1538
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
1539
- );
1709
+ console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
1540
1710
  console.log(` --key <key> API key (cv_...)`);
1541
1711
  console.log();
1542
1712
  return;
1543
1713
  }
1544
1714
 
1545
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1715
+ const hostedUrl = getFlag("--url") || API_URL;
1546
1716
  const apiKey = getFlag("--key");
1547
1717
 
1548
1718
  if (!apiKey) {
@@ -1900,6 +2070,196 @@ async function runIngest() {
1900
2070
  console.log();
1901
2071
  }
1902
2072
 
2073
+ async function runRecall() {
2074
+ let query;
2075
+
2076
+ if (!process.stdin.isTTY) {
2077
+ const raw = await new Promise((resolve) => {
2078
+ let data = "";
2079
+ process.stdin.on("data", (chunk) => (data += chunk));
2080
+ process.stdin.on("end", () => resolve(data));
2081
+ });
2082
+ try {
2083
+ const payload = JSON.parse(raw);
2084
+ query = payload.prompt || payload.query || "";
2085
+ } catch {
2086
+ query = args[1] || raw.trim();
2087
+ }
2088
+ } else {
2089
+ query = args.slice(1).join(" ");
2090
+ }
2091
+
2092
+ if (!query?.trim()) return;
2093
+
2094
+ let db;
2095
+ try {
2096
+ const { resolveConfig } = await import("@context-vault/core/core/config");
2097
+ const config = resolveConfig();
2098
+
2099
+ if (!config.vaultDirExists) return;
2100
+
2101
+ const { initDatabase, prepareStatements } =
2102
+ await import("@context-vault/core/index/db");
2103
+ const { embed } = await import("@context-vault/core/index/embed");
2104
+ const { hybridSearch } = await import("@context-vault/core/retrieve/index");
2105
+
2106
+ db = await initDatabase(config.dbPath);
2107
+ const stmts = prepareStatements(db);
2108
+ const ctx = { db, config, stmts, embed };
2109
+
2110
+ const results = await hybridSearch(ctx, query, { limit: 5 });
2111
+ if (!results.length) return;
2112
+
2113
+ const lines = ["## Context Vault\n"];
2114
+ for (const r of results) {
2115
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
2116
+ lines.push(`### ${r.title || "(untitled)"} [${r.kind}]`);
2117
+ if (entryTags.length) lines.push(`tags: ${entryTags.join(", ")}`);
2118
+ lines.push(r.body?.slice(0, 400) + (r.body?.length > 400 ? "..." : ""));
2119
+ lines.push("");
2120
+ }
2121
+
2122
+ process.stdout.write(lines.join("\n"));
2123
+ } catch {
2124
+ // fail silently — never interrupt the user's workflow
2125
+ } finally {
2126
+ try {
2127
+ db?.close();
2128
+ } catch {}
2129
+ }
2130
+ }
2131
+
2132
+ /** Returns the path to Claude Code's global settings.json */
2133
+ function claudeSettingsPath() {
2134
+ return join(HOME, ".claude", "settings.json");
2135
+ }
2136
+
2137
+ /**
2138
+ * Writes a UserPromptSubmit hook entry for context-vault recall to ~/.claude/settings.json.
2139
+ * Returns true if installed, false if already present.
2140
+ */
2141
+ function installClaudeHook() {
2142
+ const settingsPath = claudeSettingsPath();
2143
+ let settings = {};
2144
+
2145
+ if (existsSync(settingsPath)) {
2146
+ const raw = readFileSync(settingsPath, "utf-8");
2147
+ try {
2148
+ settings = JSON.parse(raw);
2149
+ } catch {
2150
+ const bak = settingsPath + ".bak";
2151
+ copyFileSync(settingsPath, bak);
2152
+ console.log(yellow(` Backed up corrupted settings to ${bak}`));
2153
+ }
2154
+ }
2155
+
2156
+ if (!settings.hooks) settings.hooks = {};
2157
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
2158
+
2159
+ const alreadyInstalled = settings.hooks.UserPromptSubmit.some((h) =>
2160
+ h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
2161
+ );
2162
+ if (alreadyInstalled) return false;
2163
+
2164
+ settings.hooks.UserPromptSubmit.push({
2165
+ hooks: [
2166
+ {
2167
+ type: "command",
2168
+ command: "context-vault recall",
2169
+ timeout: 10,
2170
+ },
2171
+ ],
2172
+ });
2173
+
2174
+ mkdirSync(dirname(settingsPath), { recursive: true });
2175
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2176
+ return true;
2177
+ }
2178
+
2179
+ /**
2180
+ * Removes the context-vault recall hook from ~/.claude/settings.json.
2181
+ * Returns true if removed, false if not found.
2182
+ */
2183
+ function removeClaudeHook() {
2184
+ const settingsPath = claudeSettingsPath();
2185
+ if (!existsSync(settingsPath)) return false;
2186
+
2187
+ let settings;
2188
+ try {
2189
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
2190
+ } catch {
2191
+ return false;
2192
+ }
2193
+
2194
+ if (!settings.hooks?.UserPromptSubmit) return false;
2195
+
2196
+ const before = settings.hooks.UserPromptSubmit.length;
2197
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
2198
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("context-vault recall")),
2199
+ );
2200
+
2201
+ if (settings.hooks.UserPromptSubmit.length === before) return false;
2202
+
2203
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2204
+ return true;
2205
+ }
2206
+
2207
+ async function runHooks() {
2208
+ const sub = args[1];
2209
+
2210
+ if (sub === "install") {
2211
+ try {
2212
+ const installed = installClaudeHook();
2213
+ if (installed) {
2214
+ console.log(`\n ${green("✓")} Claude Code memory hook installed.\n`);
2215
+ console.log(
2216
+ dim(
2217
+ " On every prompt, context-vault searches your vault for relevant entries",
2218
+ ),
2219
+ );
2220
+ console.log(
2221
+ dim(
2222
+ " and injects them as additional context alongside Claude's native memory.",
2223
+ ),
2224
+ );
2225
+ console.log(
2226
+ dim(`\n To remove: ${cyan("context-vault hooks remove")}`),
2227
+ );
2228
+ } else {
2229
+ console.log(`\n ${yellow("!")} Hook already installed.\n`);
2230
+ }
2231
+ } catch (e) {
2232
+ console.error(`\n ${red("x")} Failed to install hook: ${e.message}\n`);
2233
+ process.exit(1);
2234
+ }
2235
+ console.log();
2236
+ } else if (sub === "remove") {
2237
+ try {
2238
+ const removed = removeClaudeHook();
2239
+ if (removed) {
2240
+ console.log(`\n ${green("✓")} Claude Code memory hook removed.\n`);
2241
+ } else {
2242
+ console.log(`\n ${yellow("!")} Hook not found — nothing to remove.\n`);
2243
+ }
2244
+ } catch (e) {
2245
+ console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
2246
+ process.exit(1);
2247
+ }
2248
+ } else {
2249
+ console.log(`
2250
+ ${bold("context-vault hooks")} <install|remove>
2251
+
2252
+ Manage the Claude Code memory hook integration.
2253
+ When installed, context-vault automatically searches your vault on every user
2254
+ prompt and injects relevant entries as additional context.
2255
+
2256
+ ${bold("Commands:")}
2257
+ ${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
2258
+ ${cyan("hooks remove")} Remove the hook from ~/.claude/settings.json
2259
+ `);
2260
+ }
2261
+ }
2262
+
1903
2263
  async function runServe() {
1904
2264
  await import("../src/server/index.js");
1905
2265
  }
@@ -1928,6 +2288,12 @@ async function main() {
1928
2288
  case "serve":
1929
2289
  await runServe();
1930
2290
  break;
2291
+ case "hooks":
2292
+ await runHooks();
2293
+ break;
2294
+ case "recall":
2295
+ await runRecall();
2296
+ break;
1931
2297
  case "import":
1932
2298
  await runImport();
1933
2299
  break;
@@ -1940,6 +2306,9 @@ async function main() {
1940
2306
  case "reindex":
1941
2307
  await runReindex();
1942
2308
  break;
2309
+ case "prune":
2310
+ await runPrune();
2311
+ break;
1943
2312
  case "status":
1944
2313
  await runStatus();
1945
2314
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.10.3",
3
+ "version": "2.12.0",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -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