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 +393 -24
- 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 +13 -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 +90 -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 +144 -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
|
@@ -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
|
-
|
|
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/
|
|
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/
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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("
|
|
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") ||
|
|
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") ||
|
|
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;
|
|
@@ -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
|
|