context-vault 3.6.0 → 3.8.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/assets/vault-error-hook.mjs +106 -0
- package/assets/vault-recall-hook.mjs +67 -0
- package/bin/cli.js +734 -5
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +2 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/stats/recall.d.ts +33 -0
- package/dist/stats/recall.d.ts.map +1 -0
- package/dist/stats/recall.js +86 -0
- package/dist/stats/recall.js.map +1 -0
- package/dist/tools/clear-context.d.ts +7 -3
- package/dist/tools/clear-context.d.ts.map +1 -1
- package/dist/tools/clear-context.js +157 -8
- package/dist/tools/clear-context.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +21 -0
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +50 -1
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/recall.d.ts +25 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +257 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/session-start.d.ts +2 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +16 -5
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/package.json +2 -2
- package/src/register-tools.ts +2 -0
- package/src/stats/recall.ts +139 -0
- package/src/tools/clear-context.ts +195 -10
- package/src/tools/context-status.ts +21 -0
- package/src/tools/get-context.ts +64 -1
- package/src/tools/recall.ts +307 -0
- package/src/tools/session-start.ts +18 -5
package/bin/cli.js
CHANGED
|
@@ -301,12 +301,24 @@ const TOOLS = [
|
|
|
301
301
|
{
|
|
302
302
|
id: 'antigravity',
|
|
303
303
|
name: 'Antigravity (Gemini CLI)',
|
|
304
|
-
detect: () =>
|
|
304
|
+
detect: async () =>
|
|
305
|
+
anyDirExists(join(HOME, '.gemini', 'antigravity'), join(HOME, '.gemini')) ||
|
|
306
|
+
(await commandExistsAsync('gemini')),
|
|
305
307
|
configType: 'json',
|
|
306
308
|
configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
307
309
|
configKey: 'mcpServers',
|
|
308
|
-
rulesPath:
|
|
309
|
-
rulesMethod:
|
|
310
|
+
rulesPath: join(HOME, '.gemini', 'antigravity', 'rules', 'context-vault.md'),
|
|
311
|
+
rulesMethod: 'write',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'google-ai',
|
|
315
|
+
name: 'Google AI / Gemini CLI',
|
|
316
|
+
detect: () => existsSync(join(HOME, '.gemini', 'mcp_config.json')),
|
|
317
|
+
configType: 'json',
|
|
318
|
+
configPath: join(HOME, '.gemini', 'mcp_config.json'),
|
|
319
|
+
configKey: 'mcpServers',
|
|
320
|
+
rulesPath: join(HOME, '.gemini', 'rules', 'context-vault.md'),
|
|
321
|
+
rulesMethod: 'write',
|
|
310
322
|
},
|
|
311
323
|
{
|
|
312
324
|
id: 'cline',
|
|
@@ -396,10 +408,12 @@ ${bold('Commands:')}
|
|
|
396
408
|
${cyan('ingest')} <url> Fetch URL and save as vault entry
|
|
397
409
|
${cyan('ingest-project')} <path> Scan project directory and register as project entity
|
|
398
410
|
${cyan('reindex')} Rebuild search index from knowledge files
|
|
411
|
+
${cyan('sync')} [dir] Index .context/ files into vault DB (use --dry-run to preview)
|
|
399
412
|
${cyan('migrate-dirs')} [--dry-run] Rename plural vault dirs to singular (post-2.18.0)
|
|
400
413
|
${cyan('archive')} Archive old ephemeral/event entries (use --dry-run to preview)
|
|
401
414
|
${cyan('restore')} <id> Restore an archived entry back into the vault
|
|
402
415
|
${cyan('prune')} Remove expired entries (use --dry-run to preview)
|
|
416
|
+
${cyan('stats')} recall|co-retrieval Measure recall ratio and co-retrieval graph
|
|
403
417
|
${cyan('update')} Check for and install updates
|
|
404
418
|
${cyan('uninstall')} Remove MCP configs and optionally data
|
|
405
419
|
`);
|
|
@@ -428,6 +442,9 @@ ${bold('Commands:')}
|
|
|
428
442
|
--force Overwrite existing config without confirmation
|
|
429
443
|
--skip-embeddings Skip embedding model download (FTS-only mode)
|
|
430
444
|
--dry-run Show what setup would do without writing anything
|
|
445
|
+
--upgrade Upgrade installed agent rules to the latest bundled version
|
|
446
|
+
--no-rules Skip agent rules installation during setup
|
|
447
|
+
--no-hooks Skip recall/error hook installation during setup
|
|
431
448
|
`);
|
|
432
449
|
}
|
|
433
450
|
|
|
@@ -445,6 +462,106 @@ async function runSetup() {
|
|
|
445
462
|
}
|
|
446
463
|
console.log();
|
|
447
464
|
|
|
465
|
+
// --upgrade: only upgrade agent rules, then exit
|
|
466
|
+
if (flags.has('--upgrade')) {
|
|
467
|
+
console.log(dim(' Checking agent rules for updates...\n'));
|
|
468
|
+
const bundled = loadAgentRules();
|
|
469
|
+
if (!bundled) {
|
|
470
|
+
console.log(` ${yellow('!')} Agent rules file not found in package.\n`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const bundledVersion = extractRulesVersion(bundled);
|
|
474
|
+
|
|
475
|
+
// Check all known tool paths (not just detected tools, since a tool may have been
|
|
476
|
+
// uninstalled but its rules file still exists)
|
|
477
|
+
const allToolsWithRules = TOOLS.filter((t) => t.rulesPath);
|
|
478
|
+
let found = 0;
|
|
479
|
+
let upgraded = 0;
|
|
480
|
+
const upgradeable = [];
|
|
481
|
+
|
|
482
|
+
for (const tool of allToolsWithRules) {
|
|
483
|
+
const installed = getInstalledRulesForTool(tool);
|
|
484
|
+
if (!installed) continue;
|
|
485
|
+
found++;
|
|
486
|
+
|
|
487
|
+
const installedVersion = extractRulesVersion(installed);
|
|
488
|
+
if (installed.trim() === bundled.trim()) {
|
|
489
|
+
console.log(` ${green('✓')} ${tool.name}: up to date${bundledVersion ? ` (v${bundledVersion})` : ''}`);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
upgradeable.push({ tool, installed, installedVersion });
|
|
494
|
+
console.log(
|
|
495
|
+
` ${yellow('!')} ${tool.name}: ${installedVersion ? `v${installedVersion}` : 'unknown version'} → ${bundledVersion ? `v${bundledVersion}` : 'bundled'}`
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Show a compact diff
|
|
499
|
+
const installedLines = installed.split('\n');
|
|
500
|
+
const bundledLines = bundled.split('\n');
|
|
501
|
+
const maxLines = Math.max(installedLines.length, bundledLines.length);
|
|
502
|
+
let diffLines = 0;
|
|
503
|
+
for (let i = 0; i < maxLines; i++) {
|
|
504
|
+
const a = installedLines[i];
|
|
505
|
+
const b = bundledLines[i];
|
|
506
|
+
if (a === b) continue;
|
|
507
|
+
if (diffLines === 0) console.log();
|
|
508
|
+
if (diffLines >= 20) {
|
|
509
|
+
console.log(dim(` ... and more changes`));
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
if (a === undefined) {
|
|
513
|
+
console.log(` ${green('+')} ${b}`);
|
|
514
|
+
} else if (b === undefined) {
|
|
515
|
+
console.log(` ${red('-')} ${a}`);
|
|
516
|
+
} else {
|
|
517
|
+
console.log(` ${red('-')} ${a}`);
|
|
518
|
+
console.log(` ${green('+')} ${b}`);
|
|
519
|
+
}
|
|
520
|
+
diffLines++;
|
|
521
|
+
}
|
|
522
|
+
console.log();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (found === 0) {
|
|
526
|
+
console.log(` ${yellow('!')} No installed rules found. Run ${cyan('context-vault rules install')} first.\n`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (upgradeable.length === 0) {
|
|
531
|
+
console.log(`\n ${green('✓')} All rules are up to date.\n`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!isDryRun) {
|
|
536
|
+
const answer = isNonInteractive
|
|
537
|
+
? 'Y'
|
|
538
|
+
: await prompt(` Upgrade ${upgradeable.length} rules file(s)? (Y/n):`, 'Y');
|
|
539
|
+
if (answer.toLowerCase() === 'n') {
|
|
540
|
+
console.log(dim(' Skipped.\n'));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (const { tool } of upgradeable) {
|
|
545
|
+
try {
|
|
546
|
+
installAgentRulesForTool(tool, bundled);
|
|
547
|
+
console.log(` ${green('+')} ${tool.name} — upgraded`);
|
|
548
|
+
upgraded++;
|
|
549
|
+
} catch (e) {
|
|
550
|
+
console.log(` ${red('x')} ${tool.name} — ${e.message}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
console.log(dim(` [dry-run] Would upgrade ${upgradeable.length} rules file(s).`));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log();
|
|
558
|
+
if (upgraded > 0) {
|
|
559
|
+
console.log(dim(' Restart your AI tools to apply the updated rules.'));
|
|
560
|
+
console.log();
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
448
565
|
// Check for existing installation
|
|
449
566
|
const existingConfig = join(HOME, '.context-mcp', 'config.json');
|
|
450
567
|
if (existsSync(existingConfig) && !isNonInteractive && !isDryRun) {
|
|
@@ -653,6 +770,7 @@ async function runSetup() {
|
|
|
653
770
|
if (userLevel === 'beginner') {
|
|
654
771
|
console.log(' Install an AI tool first:');
|
|
655
772
|
console.log(dim(' Claude Code: https://docs.anthropic.com/en/docs/claude-code'));
|
|
773
|
+
console.log(dim(' Gemini CLI: https://github.com/google-gemini/gemini-cli'));
|
|
656
774
|
console.log(dim(' Cursor: https://cursor.com'));
|
|
657
775
|
console.log(dim(' Windsurf: https://codeium.com/windsurf'));
|
|
658
776
|
console.log();
|
|
@@ -1032,7 +1150,7 @@ async function runSetup() {
|
|
|
1032
1150
|
|
|
1033
1151
|
if (claudeConfigured) {
|
|
1034
1152
|
if (isDryRun) {
|
|
1035
|
-
console.log(` ${yellow('[dry-run]')} Would install Claude Code hooks (memory recall, session capture, auto-capture)`);
|
|
1153
|
+
console.log(` ${yellow('[dry-run]')} Would install Claude Code hooks (memory recall, session capture, auto-capture, vault recall, error recall)`);
|
|
1036
1154
|
console.log(` ${yellow('[dry-run]')} Would install Claude Code skills (compile-context, vault-setup)`);
|
|
1037
1155
|
} else {
|
|
1038
1156
|
// Bundled hooks prompt: one Y/n for all three hooks
|
|
@@ -1063,6 +1181,20 @@ async function runSetup() {
|
|
|
1063
1181
|
} catch (e) {
|
|
1064
1182
|
console.log(` ${red('x')} Auto-capture hook failed: ${e.message}`);
|
|
1065
1183
|
}
|
|
1184
|
+
if (!flags.has('--no-hooks')) {
|
|
1185
|
+
try {
|
|
1186
|
+
const recallInstalled = installRecallHook();
|
|
1187
|
+
if (recallInstalled) console.log(` ${green('+')} Vault recall hook installed`);
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
console.log(` ${red('x')} Recall hook failed: ${e.message}`);
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
const errorInstalled = installErrorHook();
|
|
1193
|
+
if (errorInstalled) console.log(` ${green('+')} Vault error hook installed`);
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
console.log(` ${red('x')} Error hook failed: ${e.message}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1066
1198
|
} else {
|
|
1067
1199
|
console.log(dim(` Hooks skipped. Install later: context-vault hooks install`));
|
|
1068
1200
|
}
|
|
@@ -2040,6 +2172,269 @@ async function runReindex() {
|
|
|
2040
2172
|
}
|
|
2041
2173
|
}
|
|
2042
2174
|
|
|
2175
|
+
async function runSync() {
|
|
2176
|
+
const dryRun = flags.has('--dry-run');
|
|
2177
|
+
const positional = args.slice(1).find((a) => !a.startsWith('--'));
|
|
2178
|
+
const scanDir = positional ? resolve(positional) : process.cwd();
|
|
2179
|
+
|
|
2180
|
+
const contextDir = join(scanDir, '.context');
|
|
2181
|
+
if (!existsSync(contextDir)) {
|
|
2182
|
+
console.error(red(`No .context/ directory found in ${scanDir}`));
|
|
2183
|
+
console.error(dim('The .context/ directory is created automatically when save_context is called from a workspace.'));
|
|
2184
|
+
process.exit(1);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
console.log(dim(dryRun ? 'Scanning .context/ (dry run)...' : 'Syncing .context/ to vault...'));
|
|
2188
|
+
|
|
2189
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
2190
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
2191
|
+
await import('@context-vault/core/db');
|
|
2192
|
+
const { embed } = await import('@context-vault/core/embed');
|
|
2193
|
+
const { parseFrontmatter, parseEntryFromMarkdown } = await import('@context-vault/core/frontmatter');
|
|
2194
|
+
const { categoryFor, defaultTierFor } = await import('@context-vault/core/categories');
|
|
2195
|
+
const { dirToKind, walkDir } = await import('@context-vault/core/files');
|
|
2196
|
+
const { shouldIndex } = await import('@context-vault/core/indexing');
|
|
2197
|
+
const { DEFAULT_INDEXING } = await import('@context-vault/core/constants');
|
|
2198
|
+
|
|
2199
|
+
const config = resolveConfig();
|
|
2200
|
+
if (!config.vaultDirExists) {
|
|
2201
|
+
console.error(red(`Vault directory not found: ${config.vaultDir}`));
|
|
2202
|
+
console.error('Run ' + cyan('context-vault setup') + ' to configure.');
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
const db = await initDatabase(config.dbPath);
|
|
2207
|
+
const stmts = prepareStatements(db);
|
|
2208
|
+
const ixConfig = config.indexing ?? DEFAULT_INDEXING;
|
|
2209
|
+
|
|
2210
|
+
let synced = 0;
|
|
2211
|
+
let alreadyIndexed = 0;
|
|
2212
|
+
let updated = 0;
|
|
2213
|
+
let errors = 0;
|
|
2214
|
+
let skippedIndexing = 0;
|
|
2215
|
+
|
|
2216
|
+
// Discover kind directories inside .context/
|
|
2217
|
+
let kindDirs;
|
|
2218
|
+
try {
|
|
2219
|
+
kindDirs = readdirSync(contextDir, { withFileTypes: true })
|
|
2220
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.') && !d.name.startsWith('_'));
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
console.error(red(`Failed to read .context/: ${e.message}`));
|
|
2223
|
+
db.close();
|
|
2224
|
+
process.exit(1);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const pendingEmbeds = [];
|
|
2228
|
+
|
|
2229
|
+
if (!dryRun) db.exec('BEGIN');
|
|
2230
|
+
try {
|
|
2231
|
+
for (const kindEntry of kindDirs) {
|
|
2232
|
+
const kind = dirToKind(kindEntry.name);
|
|
2233
|
+
const kindDir = join(contextDir, kindEntry.name);
|
|
2234
|
+
const mdFiles = walkDir(kindDir).filter((f) => f.filePath.endsWith('.md'));
|
|
2235
|
+
|
|
2236
|
+
for (const { filePath, relDir } of mdFiles) {
|
|
2237
|
+
let raw;
|
|
2238
|
+
try {
|
|
2239
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
2240
|
+
} catch (e) {
|
|
2241
|
+
console.error(dim(` skip: could not read ${filePath}: ${e.message}`));
|
|
2242
|
+
errors++;
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
if (!raw.startsWith('---\n')) {
|
|
2247
|
+
console.error(dim(` skip (no frontmatter): ${filePath}`));
|
|
2248
|
+
errors++;
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const { meta: fmMeta, body: rawBody } = parseFrontmatter(raw);
|
|
2253
|
+
const entryId = fmMeta.id;
|
|
2254
|
+
if (!entryId) {
|
|
2255
|
+
console.error(dim(` skip (no id in frontmatter): ${filePath}`));
|
|
2256
|
+
errors++;
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const parsed = parseEntryFromMarkdown(kind, rawBody, fmMeta);
|
|
2261
|
+
const category = categoryFor(kind);
|
|
2262
|
+
|
|
2263
|
+
// Check if entry exists in DB
|
|
2264
|
+
const existing = stmts.getEntryById.get(entryId);
|
|
2265
|
+
|
|
2266
|
+
if (existing) {
|
|
2267
|
+
// Check if content differs
|
|
2268
|
+
const bodyChanged = existing.body !== parsed.body;
|
|
2269
|
+
const titleChanged = (parsed.title || null) !== (existing.title || null);
|
|
2270
|
+
|
|
2271
|
+
if (!bodyChanged && !titleChanged) {
|
|
2272
|
+
alreadyIndexed++;
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
if (dryRun) {
|
|
2277
|
+
console.log(` ${yellow('~')} would update: ${entryId} (${parsed.title || '(untitled)'})`);
|
|
2278
|
+
updated++;
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// Update existing entry
|
|
2283
|
+
const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
|
|
2284
|
+
const meta = { ...(parsed.meta || {}) };
|
|
2285
|
+
if (relDir) meta.folder = relDir;
|
|
2286
|
+
const metaJson = Object.keys(meta).length ? JSON.stringify(meta) : null;
|
|
2287
|
+
const identity_key = fmMeta.identity_key || null;
|
|
2288
|
+
const expires_at = fmMeta.expires_at || null;
|
|
2289
|
+
|
|
2290
|
+
stmts.updateEntry.run(
|
|
2291
|
+
parsed.title || null,
|
|
2292
|
+
parsed.body,
|
|
2293
|
+
metaJson,
|
|
2294
|
+
tagsJson,
|
|
2295
|
+
fmMeta.source || 'file',
|
|
2296
|
+
category,
|
|
2297
|
+
identity_key,
|
|
2298
|
+
expires_at,
|
|
2299
|
+
existing.file_path
|
|
2300
|
+
);
|
|
2301
|
+
|
|
2302
|
+
const entryIndexed = shouldIndex(
|
|
2303
|
+
{ kind, category, bodyLength: parsed.body.length },
|
|
2304
|
+
ixConfig
|
|
2305
|
+
);
|
|
2306
|
+
|
|
2307
|
+
if (entryIndexed && category !== 'event') {
|
|
2308
|
+
const rowidResult = stmts.getRowid.get(entryId);
|
|
2309
|
+
if (rowidResult?.rowid) {
|
|
2310
|
+
const embeddingText = [parsed.title, parsed.body].filter(Boolean).join(' ');
|
|
2311
|
+
pendingEmbeds.push({ rowid: rowidResult.rowid, text: embeddingText });
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
updated++;
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Entry not in DB: index it
|
|
2320
|
+
const entryIndexed = shouldIndex(
|
|
2321
|
+
{ kind, category, bodyLength: parsed.body.length },
|
|
2322
|
+
ixConfig
|
|
2323
|
+
);
|
|
2324
|
+
|
|
2325
|
+
if (dryRun) {
|
|
2326
|
+
if (entryIndexed) {
|
|
2327
|
+
console.log(` ${green('+')} would sync: ${entryId} (${parsed.title || '(untitled)'})`);
|
|
2328
|
+
synced++;
|
|
2329
|
+
} else {
|
|
2330
|
+
console.log(` ${dim('o')} would skip indexing: ${entryId}`);
|
|
2331
|
+
skippedIndexing++;
|
|
2332
|
+
}
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
|
|
2337
|
+
const meta = { ...(parsed.meta || {}) };
|
|
2338
|
+
if (relDir) meta.folder = relDir;
|
|
2339
|
+
const metaJson = Object.keys(meta).length ? JSON.stringify(meta) : null;
|
|
2340
|
+
const created = fmMeta.created || new Date().toISOString();
|
|
2341
|
+
const identity_key = fmMeta.identity_key || null;
|
|
2342
|
+
const expires_at = fmMeta.expires_at || null;
|
|
2343
|
+
const effectiveTier = fmMeta.tier || defaultTierFor(kind);
|
|
2344
|
+
|
|
2345
|
+
// The entry should point to the vault file path (if it exists there), else use the .context path
|
|
2346
|
+
const vaultFilePath = existing?.file_path || fmMeta.file_path || filePath;
|
|
2347
|
+
|
|
2348
|
+
try {
|
|
2349
|
+
const upsertEntry = db.prepare(
|
|
2350
|
+
`INSERT OR IGNORE INTO vault (id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, tier, indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2351
|
+
);
|
|
2352
|
+
const result = upsertEntry.run(
|
|
2353
|
+
entryId,
|
|
2354
|
+
kind,
|
|
2355
|
+
category,
|
|
2356
|
+
parsed.title || null,
|
|
2357
|
+
parsed.body,
|
|
2358
|
+
metaJson,
|
|
2359
|
+
tagsJson,
|
|
2360
|
+
fmMeta.source || 'file',
|
|
2361
|
+
vaultFilePath,
|
|
2362
|
+
identity_key,
|
|
2363
|
+
expires_at,
|
|
2364
|
+
created,
|
|
2365
|
+
fmMeta.updated || created,
|
|
2366
|
+
effectiveTier,
|
|
2367
|
+
entryIndexed ? 1 : 0
|
|
2368
|
+
);
|
|
2369
|
+
|
|
2370
|
+
if (result.changes > 0) {
|
|
2371
|
+
if (entryIndexed && category !== 'event') {
|
|
2372
|
+
const rowidResult = stmts.getRowid.get(entryId);
|
|
2373
|
+
if (rowidResult?.rowid) {
|
|
2374
|
+
const embeddingText = [parsed.title, parsed.body].filter(Boolean).join(' ');
|
|
2375
|
+
pendingEmbeds.push({ rowid: rowidResult.rowid, text: embeddingText });
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (!entryIndexed) skippedIndexing++;
|
|
2379
|
+
synced++;
|
|
2380
|
+
} else {
|
|
2381
|
+
alreadyIndexed++;
|
|
2382
|
+
}
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
console.error(dim(` error indexing ${entryId}: ${e.message}`));
|
|
2385
|
+
errors++;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// Generate embeddings in batch
|
|
2391
|
+
if (!dryRun && pendingEmbeds.length > 0) {
|
|
2392
|
+
const { embedBatch: batchEmbed } = await import('@context-vault/core/embed');
|
|
2393
|
+
const BATCH_SIZE = 32;
|
|
2394
|
+
for (let i = 0; i < pendingEmbeds.length; i += BATCH_SIZE) {
|
|
2395
|
+
const batch = pendingEmbeds.slice(i, i + BATCH_SIZE);
|
|
2396
|
+
const texts = batch.map((b) => b.text);
|
|
2397
|
+
try {
|
|
2398
|
+
const embeddings = await batchEmbed(texts);
|
|
2399
|
+
for (let j = 0; j < batch.length; j++) {
|
|
2400
|
+
if (embeddings[j]) {
|
|
2401
|
+
try { deleteVec(stmts, batch[j].rowid); } catch {}
|
|
2402
|
+
insertVec(stmts, batch[j].rowid, embeddings[j]);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
} catch (e) {
|
|
2406
|
+
console.warn(dim(` embedding batch failed: ${e.message}`));
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (!dryRun) db.exec('COMMIT');
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
if (!dryRun) {
|
|
2414
|
+
try { db.exec('ROLLBACK'); } catch {}
|
|
2415
|
+
}
|
|
2416
|
+
throw e;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
db.close();
|
|
2420
|
+
|
|
2421
|
+
if (dryRun) {
|
|
2422
|
+
console.log(yellow('Dry run results (no changes made):'));
|
|
2423
|
+
console.log(` Would sync: ${synced}`);
|
|
2424
|
+
console.log(` Would update: ${updated}`);
|
|
2425
|
+
console.log(` Already indexed: ${alreadyIndexed}`);
|
|
2426
|
+
if (skippedIndexing) console.log(` Would skip indexing: ${skippedIndexing}`);
|
|
2427
|
+
if (errors) console.log(` ${red('Errors:')} ${errors}`);
|
|
2428
|
+
} else {
|
|
2429
|
+
console.log(green('Sync complete'));
|
|
2430
|
+
console.log(` ${green('+')} ${synced} synced`);
|
|
2431
|
+
if (updated) console.log(` ${yellow('~')} ${updated} updated`);
|
|
2432
|
+
console.log(` ${dim('.')} ${alreadyIndexed} already indexed`);
|
|
2433
|
+
if (skippedIndexing) console.log(` ${dim('o')} ${skippedIndexing} skipped indexing`);
|
|
2434
|
+
if (errors) console.log(` ${red('!')} ${errors} errors`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2043
2438
|
async function runMigrateDirs() {
|
|
2044
2439
|
const dryRun = flags.has('--dry-run');
|
|
2045
2440
|
|
|
@@ -2542,7 +2937,9 @@ async function runUninstall() {
|
|
|
2542
2937
|
const captureRemoved = removeSessionCaptureHook();
|
|
2543
2938
|
const flushRemoved = removeSessionEndHook();
|
|
2544
2939
|
const autoCaptureRemoved = removePostToolCallHook();
|
|
2545
|
-
|
|
2940
|
+
const recallHookRemoved = removeRecallHook();
|
|
2941
|
+
const errorHookRemoved = removeErrorHook();
|
|
2942
|
+
if (recallRemoved || captureRemoved || flushRemoved || autoCaptureRemoved || recallHookRemoved || errorHookRemoved) {
|
|
2546
2943
|
console.log(` ${green('+')} Removed Claude Code hooks`);
|
|
2547
2944
|
} else {
|
|
2548
2945
|
console.log(` ${dim('-')} No Claude Code hooks to remove`);
|
|
@@ -4280,6 +4677,35 @@ function loadAgentRules() {
|
|
|
4280
4677
|
return readFileSync(rulesPath, 'utf-8');
|
|
4281
4678
|
}
|
|
4282
4679
|
|
|
4680
|
+
/**
|
|
4681
|
+
* Extract the version string from a rules file content.
|
|
4682
|
+
* Looks for <!-- context-vault-rules vX.Y --> comment on the first line.
|
|
4683
|
+
* Returns the version string (e.g. "1.0") or null if not found.
|
|
4684
|
+
*/
|
|
4685
|
+
function extractRulesVersion(content) {
|
|
4686
|
+
if (!content) return null;
|
|
4687
|
+
const match = content.match(/<!--\s*context-vault-rules\s+v([\d.]+)\s*-->/);
|
|
4688
|
+
return match ? match[1] : null;
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
/**
|
|
4692
|
+
* Get the installed rules content for a tool, handling both write and append methods.
|
|
4693
|
+
* For append-based tools (Windsurf), extracts only the delimited section.
|
|
4694
|
+
* Returns the rules content or null if not installed.
|
|
4695
|
+
*/
|
|
4696
|
+
function getInstalledRulesForTool(tool) {
|
|
4697
|
+
const rulesPath = tool.rulesPath;
|
|
4698
|
+
if (!rulesPath || !existsSync(rulesPath)) return null;
|
|
4699
|
+
const content = readFileSync(rulesPath, 'utf-8');
|
|
4700
|
+
if (tool.rulesMethod === 'append') {
|
|
4701
|
+
const match = content.match(
|
|
4702
|
+
new RegExp(`${RULES_DELIMITER_START}\\n([\\s\\S]*?)\\n${RULES_DELIMITER_END}`)
|
|
4703
|
+
);
|
|
4704
|
+
return match ? match[1] : null;
|
|
4705
|
+
}
|
|
4706
|
+
return content;
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4283
4709
|
/**
|
|
4284
4710
|
* Return the path where agent rules are/would be installed for a given tool.
|
|
4285
4711
|
* Returns null for tools with no rules install path.
|
|
@@ -4656,6 +5082,159 @@ function removePostToolCallHook() {
|
|
|
4656
5082
|
return true;
|
|
4657
5083
|
}
|
|
4658
5084
|
|
|
5085
|
+
/**
|
|
5086
|
+
* Install the vault-recall-hook.mjs into ~/.claude/hooks/ and register it
|
|
5087
|
+
* as a UserPromptSubmit hook in ~/.claude/settings.json.
|
|
5088
|
+
* Returns true if installed, false if already present.
|
|
5089
|
+
*/
|
|
5090
|
+
function installRecallHook() {
|
|
5091
|
+
const srcPath = join(ROOT, 'assets', 'vault-recall-hook.mjs');
|
|
5092
|
+
if (!existsSync(srcPath)) return false;
|
|
5093
|
+
|
|
5094
|
+
const hooksDir = join(HOME, '.claude', 'hooks');
|
|
5095
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
5096
|
+
const destPath = join(hooksDir, 'vault-recall-hook.mjs');
|
|
5097
|
+
copyFileSync(srcPath, destPath);
|
|
5098
|
+
|
|
5099
|
+
const settingsPath = claudeSettingsPath();
|
|
5100
|
+
let settings = {};
|
|
5101
|
+
if (existsSync(settingsPath)) {
|
|
5102
|
+
try {
|
|
5103
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
5104
|
+
} catch {
|
|
5105
|
+
const bak = settingsPath + '.bak';
|
|
5106
|
+
copyFileSync(settingsPath, bak);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
if (!settings.hooks) settings.hooks = {};
|
|
5111
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
5112
|
+
|
|
5113
|
+
const hookCmd = `node ${destPath}`;
|
|
5114
|
+
const alreadyInstalled = settings.hooks.UserPromptSubmit.some((h) =>
|
|
5115
|
+
h.hooks?.some((hh) => hh.command?.includes('vault-recall-hook'))
|
|
5116
|
+
);
|
|
5117
|
+
if (alreadyInstalled) return false;
|
|
5118
|
+
|
|
5119
|
+
settings.hooks.UserPromptSubmit.push({
|
|
5120
|
+
hooks: [
|
|
5121
|
+
{
|
|
5122
|
+
type: 'command',
|
|
5123
|
+
command: hookCmd,
|
|
5124
|
+
timeout: 5,
|
|
5125
|
+
},
|
|
5126
|
+
],
|
|
5127
|
+
});
|
|
5128
|
+
|
|
5129
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
5130
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
5131
|
+
return true;
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5134
|
+
/**
|
|
5135
|
+
* Remove the vault-recall-hook UserPromptSubmit hook from settings.json.
|
|
5136
|
+
* Returns true if removed, false if not found.
|
|
5137
|
+
*/
|
|
5138
|
+
function removeRecallHook() {
|
|
5139
|
+
const settingsPath = claudeSettingsPath();
|
|
5140
|
+
if (!existsSync(settingsPath)) return false;
|
|
5141
|
+
|
|
5142
|
+
let settings;
|
|
5143
|
+
try {
|
|
5144
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
5145
|
+
} catch {
|
|
5146
|
+
return false;
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
if (!settings.hooks?.UserPromptSubmit) return false;
|
|
5150
|
+
|
|
5151
|
+
const before = settings.hooks.UserPromptSubmit.length;
|
|
5152
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
5153
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes('vault-recall-hook'))
|
|
5154
|
+
);
|
|
5155
|
+
|
|
5156
|
+
if (settings.hooks.UserPromptSubmit.length === before) return false;
|
|
5157
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
5158
|
+
return true;
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
/**
|
|
5162
|
+
* Install the vault-error-hook.mjs into ~/.claude/hooks/ and register it
|
|
5163
|
+
* as a PostToolUse hook (matcher: Bash) in ~/.claude/settings.json.
|
|
5164
|
+
* Returns true if installed, false if already present.
|
|
5165
|
+
*/
|
|
5166
|
+
function installErrorHook() {
|
|
5167
|
+
const srcPath = join(ROOT, 'assets', 'vault-error-hook.mjs');
|
|
5168
|
+
if (!existsSync(srcPath)) return false;
|
|
5169
|
+
|
|
5170
|
+
const hooksDir = join(HOME, '.claude', 'hooks');
|
|
5171
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
5172
|
+
const destPath = join(hooksDir, 'vault-error-hook.mjs');
|
|
5173
|
+
copyFileSync(srcPath, destPath);
|
|
5174
|
+
|
|
5175
|
+
const settingsPath = claudeSettingsPath();
|
|
5176
|
+
let settings = {};
|
|
5177
|
+
if (existsSync(settingsPath)) {
|
|
5178
|
+
try {
|
|
5179
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
5180
|
+
} catch {
|
|
5181
|
+
const bak = settingsPath + '.bak';
|
|
5182
|
+
copyFileSync(settingsPath, bak);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
if (!settings.hooks) settings.hooks = {};
|
|
5187
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
5188
|
+
|
|
5189
|
+
const hookCmd = `node ${destPath}`;
|
|
5190
|
+
const alreadyInstalled = settings.hooks.PostToolUse.some((h) =>
|
|
5191
|
+
h.hooks?.some((hh) => hh.command?.includes('vault-error-hook'))
|
|
5192
|
+
);
|
|
5193
|
+
if (alreadyInstalled) return false;
|
|
5194
|
+
|
|
5195
|
+
settings.hooks.PostToolUse.push({
|
|
5196
|
+
matcher: 'Bash',
|
|
5197
|
+
hooks: [
|
|
5198
|
+
{
|
|
5199
|
+
type: 'command',
|
|
5200
|
+
command: hookCmd,
|
|
5201
|
+
timeout: 5,
|
|
5202
|
+
},
|
|
5203
|
+
],
|
|
5204
|
+
});
|
|
5205
|
+
|
|
5206
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
5207
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
5208
|
+
return true;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
/**
|
|
5212
|
+
* Remove the vault-error-hook PostToolUse hook from settings.json.
|
|
5213
|
+
* Returns true if removed, false if not found.
|
|
5214
|
+
*/
|
|
5215
|
+
function removeErrorHook() {
|
|
5216
|
+
const settingsPath = claudeSettingsPath();
|
|
5217
|
+
if (!existsSync(settingsPath)) return false;
|
|
5218
|
+
|
|
5219
|
+
let settings;
|
|
5220
|
+
try {
|
|
5221
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
5222
|
+
} catch {
|
|
5223
|
+
return false;
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
if (!settings.hooks?.PostToolUse) return false;
|
|
5227
|
+
|
|
5228
|
+
const before = settings.hooks.PostToolUse.length;
|
|
5229
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
5230
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes('vault-error-hook'))
|
|
5231
|
+
);
|
|
5232
|
+
|
|
5233
|
+
if (settings.hooks.PostToolUse.length === before) return false;
|
|
5234
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
5235
|
+
return true;
|
|
5236
|
+
}
|
|
5237
|
+
|
|
4659
5238
|
async function runSkills() {
|
|
4660
5239
|
const sub = args[1];
|
|
4661
5240
|
|
|
@@ -4953,6 +5532,25 @@ async function runHooksInstall() {
|
|
|
4953
5532
|
}
|
|
4954
5533
|
console.log();
|
|
4955
5534
|
}
|
|
5535
|
+
|
|
5536
|
+
// Proactive surfacing hooks (vault recall + error recall)
|
|
5537
|
+
try {
|
|
5538
|
+
const recallInstalled = installRecallHook();
|
|
5539
|
+
if (recallInstalled) {
|
|
5540
|
+
console.log(` ${green('✓')} Vault recall hook installed (proactive surfacing on prompts)`);
|
|
5541
|
+
}
|
|
5542
|
+
} catch (e) {
|
|
5543
|
+
console.error(` ${red('x')} Vault recall hook failed: ${e.message}`);
|
|
5544
|
+
}
|
|
5545
|
+
try {
|
|
5546
|
+
const errorInstalled = installErrorHook();
|
|
5547
|
+
if (errorInstalled) {
|
|
5548
|
+
console.log(` ${green('✓')} Vault error hook installed (surfaces past errors on Bash failures)`);
|
|
5549
|
+
}
|
|
5550
|
+
} catch (e) {
|
|
5551
|
+
console.error(` ${red('x')} Vault error hook failed: ${e.message}`);
|
|
5552
|
+
}
|
|
5553
|
+
console.log();
|
|
4956
5554
|
}
|
|
4957
5555
|
|
|
4958
5556
|
async function runHooksUninstall() {
|
|
@@ -4994,6 +5592,24 @@ async function runHooksUninstall() {
|
|
|
4994
5592
|
} catch (e) {
|
|
4995
5593
|
console.error(`\n ${red('x')} Failed to remove auto-capture hook: ${e.message}\n`);
|
|
4996
5594
|
}
|
|
5595
|
+
|
|
5596
|
+
try {
|
|
5597
|
+
const recallHookRemoved = removeRecallHook();
|
|
5598
|
+
if (recallHookRemoved) {
|
|
5599
|
+
console.log(`\n ${green('✓')} Vault recall hook removed.\n`);
|
|
5600
|
+
}
|
|
5601
|
+
} catch (e) {
|
|
5602
|
+
console.error(`\n ${red('x')} Failed to remove recall hook: ${e.message}\n`);
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
try {
|
|
5606
|
+
const errorHookRemoved = removeErrorHook();
|
|
5607
|
+
if (errorHookRemoved) {
|
|
5608
|
+
console.log(`\n ${green('✓')} Vault error hook removed.\n`);
|
|
5609
|
+
}
|
|
5610
|
+
} catch (e) {
|
|
5611
|
+
console.error(`\n ${red('x')} Failed to remove error hook: ${e.message}\n`);
|
|
5612
|
+
}
|
|
4997
5613
|
}
|
|
4998
5614
|
|
|
4999
5615
|
async function runHooks() {
|
|
@@ -6313,6 +6929,113 @@ ${progArgs.map(a => ` <string>${a}</string>`).join('\n')}
|
|
|
6313
6929
|
}
|
|
6314
6930
|
}
|
|
6315
6931
|
|
|
6932
|
+
async function runStats() {
|
|
6933
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
6934
|
+
const { initDatabase } = await import('@context-vault/core/db');
|
|
6935
|
+
const { gatherRecallSummary, gatherCoRetrievalSummary } = await import('../dist/stats/recall.js');
|
|
6936
|
+
|
|
6937
|
+
const sub = args[1];
|
|
6938
|
+
if (!sub || sub === 'recall') {
|
|
6939
|
+
await runStatsRecall({ resolveConfig, initDatabase, gatherRecallSummary });
|
|
6940
|
+
} else if (sub === 'co-retrieval') {
|
|
6941
|
+
await runStatsCoRetrieval({ resolveConfig, initDatabase, gatherCoRetrievalSummary });
|
|
6942
|
+
} else {
|
|
6943
|
+
console.error(red(` Unknown stats subcommand: ${sub}`));
|
|
6944
|
+
console.error(` Available: recall, co-retrieval`);
|
|
6945
|
+
process.exit(1);
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
|
|
6949
|
+
async function runStatsRecall({ resolveConfig, initDatabase, gatherRecallSummary }) {
|
|
6950
|
+
const config = resolveConfig();
|
|
6951
|
+
let db;
|
|
6952
|
+
try {
|
|
6953
|
+
db = await initDatabase(config.dbPath);
|
|
6954
|
+
} catch (e) {
|
|
6955
|
+
console.error(red(` Database not accessible: ${e.message}`));
|
|
6956
|
+
process.exit(1);
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6959
|
+
let s;
|
|
6960
|
+
try {
|
|
6961
|
+
s = gatherRecallSummary({ db, config });
|
|
6962
|
+
} finally {
|
|
6963
|
+
db.close();
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
const ratioPct = Math.round(s.ratio * 100);
|
|
6967
|
+
const targetPct = Math.round(s.target * 100);
|
|
6968
|
+
const statusIcon = s.ratio >= s.target ? green('✓') : yellow('·');
|
|
6969
|
+
console.log();
|
|
6970
|
+
console.log(` ${bold('◇ context-vault stats recall')}`);
|
|
6971
|
+
console.log();
|
|
6972
|
+
console.log(` ${statusIcon} Recall ratio: ${bold(s.ratio.toFixed(2))} (target: ${s.target.toFixed(2)})`);
|
|
6973
|
+
console.log(` Total entries: ${s.total_entries}`);
|
|
6974
|
+
console.log(` Recalled (1+): ${s.recalled_entries} (${ratioPct}%)`);
|
|
6975
|
+
console.log(` Never recalled: ${s.never_recalled} (${100 - ratioPct}%)`);
|
|
6976
|
+
console.log(` Avg recall count: ${s.avg_recall_count} (among recalled entries)`);
|
|
6977
|
+
|
|
6978
|
+
if (s.top_recalled.length) {
|
|
6979
|
+
console.log();
|
|
6980
|
+
console.log(` ${bold('Top recalled:')}`);
|
|
6981
|
+
for (let i = 0; i < s.top_recalled.length; i++) {
|
|
6982
|
+
const e = s.top_recalled[i];
|
|
6983
|
+
const title = (e.title || '(untitled)').slice(0, 50);
|
|
6984
|
+
console.log(` ${i + 1}. "${title}" (recall: ${e.recall_count}, sessions: ${e.recall_sessions})`);
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6988
|
+
if (s.dead_entry_count > 0) {
|
|
6989
|
+
console.log();
|
|
6990
|
+
console.log(` ${bold('Dead entries')} ${dim('(saved >30 days ago, never recalled):')}`);
|
|
6991
|
+
console.log(` - ${s.dead_entry_count} entries across ${s.dead_bucket_count} buckets`);
|
|
6992
|
+
if (s.top_dead_buckets.length) {
|
|
6993
|
+
const bucketStr = s.top_dead_buckets.map((b) => `${b.bucket} (${b.count})`).join(', ');
|
|
6994
|
+
console.log(` - Top dead buckets: ${bucketStr}`);
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
console.log();
|
|
6999
|
+
}
|
|
7000
|
+
|
|
7001
|
+
async function runStatsCoRetrieval({ resolveConfig, initDatabase, gatherCoRetrievalSummary }) {
|
|
7002
|
+
const config = resolveConfig();
|
|
7003
|
+
let db;
|
|
7004
|
+
try {
|
|
7005
|
+
db = await initDatabase(config.dbPath);
|
|
7006
|
+
} catch (e) {
|
|
7007
|
+
console.error(red(` Database not accessible: ${e.message}`));
|
|
7008
|
+
process.exit(1);
|
|
7009
|
+
}
|
|
7010
|
+
|
|
7011
|
+
let s;
|
|
7012
|
+
try {
|
|
7013
|
+
s = gatherCoRetrievalSummary({ db, config });
|
|
7014
|
+
} finally {
|
|
7015
|
+
db.close();
|
|
7016
|
+
}
|
|
7017
|
+
|
|
7018
|
+
console.log();
|
|
7019
|
+
console.log(` ${bold('◇ context-vault stats co-retrieval')}`);
|
|
7020
|
+
console.log();
|
|
7021
|
+
console.log(` Co-retrieval pairs: ${bold(String(s.total_pairs))}`);
|
|
7022
|
+
|
|
7023
|
+
if (s.top_pairs.length) {
|
|
7024
|
+
console.log();
|
|
7025
|
+
console.log(` ${bold('Strongest pairs:')}`);
|
|
7026
|
+
for (let i = 0; i < s.top_pairs.length; i++) {
|
|
7027
|
+
const p = s.top_pairs[i];
|
|
7028
|
+
const titleA = (p.title_a || '(untitled)').slice(0, 40);
|
|
7029
|
+
const titleB = (p.title_b || '(untitled)').slice(0, 40);
|
|
7030
|
+
console.log(` ${i + 1}. "${titleA}" <-> "${titleB}" (weight: ${p.weight})`);
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
|
|
7034
|
+
console.log();
|
|
7035
|
+
console.log(` Graph density: ${s.graph_density.toFixed(4)} ${dim('(sparse, expected for early usage)')}`);
|
|
7036
|
+
console.log();
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6316
7039
|
async function runServe() {
|
|
6317
7040
|
await import('../dist/server.js');
|
|
6318
7041
|
}
|
|
@@ -6402,6 +7125,9 @@ async function main() {
|
|
|
6402
7125
|
case 'reindex':
|
|
6403
7126
|
await runReindex();
|
|
6404
7127
|
break;
|
|
7128
|
+
case 'sync':
|
|
7129
|
+
await runSync();
|
|
7130
|
+
break;
|
|
6405
7131
|
case 'migrate-dirs':
|
|
6406
7132
|
await runMigrateDirs();
|
|
6407
7133
|
break;
|
|
@@ -6444,6 +7170,9 @@ async function main() {
|
|
|
6444
7170
|
case 'debug':
|
|
6445
7171
|
await runDebug();
|
|
6446
7172
|
break;
|
|
7173
|
+
case 'stats':
|
|
7174
|
+
await runStats();
|
|
7175
|
+
break;
|
|
6447
7176
|
default:
|
|
6448
7177
|
console.error(red(`Unknown command: ${command}`));
|
|
6449
7178
|
console.error(`Run ${cyan('context-vault --help')} for usage.`);
|