context-vault 2.13.0 → 2.14.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/skills/compile-context/skill.md +45 -0
- package/bin/cli.js +228 -83
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/server/tools/create-snapshot.js +222 -0
- package/node_modules/@context-vault/core/src/server/tools.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compile-context
|
|
3
|
+
description: >
|
|
4
|
+
Compiles scattered vault entries on a topic into a single authoritative brief
|
|
5
|
+
for isolated retrieval in a fresh context window. Use when starting a new work
|
|
6
|
+
session on a project, preparing a handoff, or loading focused context without
|
|
7
|
+
noise. Also audits for stale or contradicting entries.
|
|
8
|
+
Triggers: "compile context", "create a brief", "context snapshot", "context bucket",
|
|
9
|
+
"make a brief for X", "load context for X".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# compile-context skill
|
|
13
|
+
|
|
14
|
+
When the user asks to compile context or create a brief for a topic, call `create_snapshot` to synthesize a context brief from the vault.
|
|
15
|
+
|
|
16
|
+
## Step 1 — Identify the topic
|
|
17
|
+
|
|
18
|
+
If the user provided a topic or project name, use it. If not, ask:
|
|
19
|
+
|
|
20
|
+
> "What topic or project should I compile context for?"
|
|
21
|
+
|
|
22
|
+
Derive a slug: lowercase, hyphens, no spaces (e.g. `neonode`, `context-vault`, `klarhimmel-infra`).
|
|
23
|
+
|
|
24
|
+
## Step 2 — Call create_snapshot
|
|
25
|
+
|
|
26
|
+
Call `create_snapshot` with:
|
|
27
|
+
|
|
28
|
+
- `topic`: the topic name the user provided
|
|
29
|
+
- `identity_key`: `snapshot-<slug>` (e.g. `snapshot-context-vault`)
|
|
30
|
+
- `tags` (optional): any relevant tags the user mentions
|
|
31
|
+
- `kinds` (optional): restrict to specific entry kinds if the user requests it
|
|
32
|
+
|
|
33
|
+
The tool handles retrieval, deduplication, LLM synthesis, and saving automatically.
|
|
34
|
+
|
|
35
|
+
## Step 3 — Report
|
|
36
|
+
|
|
37
|
+
After the tool returns, tell the user:
|
|
38
|
+
|
|
39
|
+
- The ULID of the saved brief
|
|
40
|
+
- How many entries were synthesized
|
|
41
|
+
- The exact call to retrieve it in a future session:
|
|
42
|
+
```
|
|
43
|
+
get_context(identity_key: "snapshot-<slug>")
|
|
44
|
+
```
|
|
45
|
+
- Suggest pinning the identity key in the relevant CLAUDE.md or MEMORY.md for zero-cost retrieval in fresh windows.
|
package/bin/cli.js
CHANGED
|
@@ -235,7 +235,9 @@ ${bold("Commands:")}
|
|
|
235
235
|
${cyan("connect")} --key cv_... Connect AI tools to hosted vault
|
|
236
236
|
${cyan("switch")} local|hosted Switch between local and hosted MCP modes
|
|
237
237
|
${cyan("serve")} Start the MCP server (used by AI clients)
|
|
238
|
-
${cyan("hooks")} install|
|
|
238
|
+
${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
|
|
239
|
+
${cyan("claude")} install|uninstall Alias for hooks install|uninstall
|
|
240
|
+
${cyan("skills")} install Install bundled Claude Code skills
|
|
239
241
|
${cyan("flush")} Check vault health and confirm DB is accessible
|
|
240
242
|
${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
|
|
241
243
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
@@ -731,6 +733,37 @@ async function runSetup() {
|
|
|
731
733
|
}
|
|
732
734
|
}
|
|
733
735
|
|
|
736
|
+
// Claude Code skills (opt-in)
|
|
737
|
+
if (claudeConfigured && !isNonInteractive) {
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(dim(" Install Claude Code skills? (recommended)"));
|
|
740
|
+
console.log(
|
|
741
|
+
dim(" compile-context — compile vault entries into a project brief"),
|
|
742
|
+
);
|
|
743
|
+
console.log();
|
|
744
|
+
const skillAnswer = await prompt(
|
|
745
|
+
" Install Claude Code skills? (Y/n):",
|
|
746
|
+
"Y",
|
|
747
|
+
);
|
|
748
|
+
const installSkillsFlag = skillAnswer.toLowerCase() !== "n";
|
|
749
|
+
if (installSkillsFlag) {
|
|
750
|
+
try {
|
|
751
|
+
const names = installSkills();
|
|
752
|
+
if (names.length > 0) {
|
|
753
|
+
for (const name of names) {
|
|
754
|
+
console.log(`\n ${green("+")} ${name} skill installed`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
console.log(`\n ${red("x")} Skills install failed: ${e.message}`);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
console.log(
|
|
762
|
+
dim(` Skipped — install later: context-vault skills install`),
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
734
767
|
// Seed entry
|
|
735
768
|
const seeded = createSeedEntries(resolvedVaultDir);
|
|
736
769
|
if (seeded > 0) {
|
|
@@ -2126,16 +2159,25 @@ async function runRecall() {
|
|
|
2126
2159
|
const results = await hybridSearch(ctx, query, { limit: 5 });
|
|
2127
2160
|
if (!results.length) return;
|
|
2128
2161
|
|
|
2129
|
-
const
|
|
2162
|
+
const MAX_TOTAL = 2000;
|
|
2163
|
+
const ENTRY_BODY_LIMIT = 400;
|
|
2164
|
+
const entries = [];
|
|
2165
|
+
let totalChars = 0;
|
|
2166
|
+
|
|
2130
2167
|
for (const r of results) {
|
|
2131
2168
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2169
|
+
const tagsAttr = entryTags.length ? ` tags="${entryTags.join(",")}"` : "";
|
|
2170
|
+
const body = r.body?.slice(0, ENTRY_BODY_LIMIT) ?? "";
|
|
2171
|
+
const entry = `<entry kind="${r.kind || "knowledge"}"${tagsAttr}>\n${body}\n</entry>`;
|
|
2172
|
+
if (totalChars + entry.length > MAX_TOTAL) break;
|
|
2173
|
+
entries.push(entry);
|
|
2174
|
+
totalChars += entry.length;
|
|
2136
2175
|
}
|
|
2137
2176
|
|
|
2138
|
-
|
|
2177
|
+
if (!entries.length) return;
|
|
2178
|
+
|
|
2179
|
+
const block = `<context-vault>\n${entries.join("\n")}\n</context-vault>\n`;
|
|
2180
|
+
process.stdout.write(block);
|
|
2139
2181
|
} catch {
|
|
2140
2182
|
// fail silently — never interrupt the user's workflow
|
|
2141
2183
|
} finally {
|
|
@@ -2176,6 +2218,38 @@ async function runFlush() {
|
|
|
2176
2218
|
}
|
|
2177
2219
|
}
|
|
2178
2220
|
|
|
2221
|
+
/**
|
|
2222
|
+
* Copies all skills from the bundled assets/skills/ directory into ~/.claude/skills/.
|
|
2223
|
+
* Returns an array of installed skill names.
|
|
2224
|
+
*/
|
|
2225
|
+
function installSkills() {
|
|
2226
|
+
const assetsSkillsDir = join(ROOT, "assets", "skills");
|
|
2227
|
+
const targetDir = join(HOME, ".claude", "skills");
|
|
2228
|
+
|
|
2229
|
+
if (!existsSync(assetsSkillsDir)) return [];
|
|
2230
|
+
|
|
2231
|
+
const skillNames = readdirSync(assetsSkillsDir).filter((name) => {
|
|
2232
|
+
try {
|
|
2233
|
+
return statSync(join(assetsSkillsDir, name)).isDirectory();
|
|
2234
|
+
} catch {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
const installed = [];
|
|
2240
|
+
for (const skillName of skillNames) {
|
|
2241
|
+
const srcDir = join(assetsSkillsDir, skillName);
|
|
2242
|
+
const destDir = join(targetDir, skillName);
|
|
2243
|
+
mkdirSync(destDir, { recursive: true });
|
|
2244
|
+
const files = readdirSync(srcDir);
|
|
2245
|
+
for (const file of files) {
|
|
2246
|
+
copyFileSync(join(srcDir, file), join(destDir, file));
|
|
2247
|
+
}
|
|
2248
|
+
installed.push(skillName);
|
|
2249
|
+
}
|
|
2250
|
+
return installed;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2179
2253
|
/** Returns the path to Claude Code's global settings.json */
|
|
2180
2254
|
function claudeSettingsPath() {
|
|
2181
2255
|
return join(HOME, ".claude", "settings.json");
|
|
@@ -2321,110 +2395,175 @@ function removeClaudeHook() {
|
|
|
2321
2395
|
return true;
|
|
2322
2396
|
}
|
|
2323
2397
|
|
|
2324
|
-
async function
|
|
2398
|
+
async function runSkills() {
|
|
2325
2399
|
const sub = args[1];
|
|
2326
2400
|
|
|
2327
2401
|
if (sub === "install") {
|
|
2402
|
+
console.log();
|
|
2328
2403
|
try {
|
|
2329
|
-
const
|
|
2330
|
-
if (
|
|
2331
|
-
console.log(
|
|
2332
|
-
console.log(
|
|
2333
|
-
dim(
|
|
2334
|
-
" On every prompt, context-vault searches your vault for relevant entries",
|
|
2335
|
-
),
|
|
2336
|
-
);
|
|
2337
|
-
console.log(
|
|
2338
|
-
dim(
|
|
2339
|
-
" and injects them as additional context alongside Claude's native memory.",
|
|
2340
|
-
),
|
|
2341
|
-
);
|
|
2342
|
-
console.log(
|
|
2343
|
-
dim(`\n To remove: ${cyan("context-vault hooks remove")}`),
|
|
2344
|
-
);
|
|
2404
|
+
const names = installSkills();
|
|
2405
|
+
if (names.length === 0) {
|
|
2406
|
+
console.log(` ${yellow("!")} No bundled skills found.\n`);
|
|
2345
2407
|
} else {
|
|
2346
|
-
|
|
2347
|
-
}
|
|
2348
|
-
} catch (e) {
|
|
2349
|
-
console.error(`\n ${red("x")} Failed to install hook: ${e.message}\n`);
|
|
2350
|
-
process.exit(1);
|
|
2351
|
-
}
|
|
2352
|
-
console.log();
|
|
2353
|
-
|
|
2354
|
-
// Prompt for optional session auto-flush (SessionEnd) hook
|
|
2355
|
-
const installFlush =
|
|
2356
|
-
flags.has("--flush") ||
|
|
2357
|
-
(await prompt(
|
|
2358
|
-
" Install session auto-flush hook? (runs context-vault flush at session end) (y/N):",
|
|
2359
|
-
"n",
|
|
2360
|
-
));
|
|
2361
|
-
const shouldInstallFlush =
|
|
2362
|
-
installFlush === true ||
|
|
2363
|
-
(typeof installFlush === "string" &&
|
|
2364
|
-
installFlush.toLowerCase().startsWith("y"));
|
|
2365
|
-
|
|
2366
|
-
if (shouldInstallFlush) {
|
|
2367
|
-
try {
|
|
2368
|
-
const flushInstalled = installSessionEndHook();
|
|
2369
|
-
if (flushInstalled) {
|
|
2370
|
-
console.log(
|
|
2371
|
-
`\n ${green("✓")} Session auto-flush hook installed (SessionEnd).\n`,
|
|
2372
|
-
);
|
|
2408
|
+
for (const name of names) {
|
|
2373
2409
|
console.log(
|
|
2374
|
-
|
|
2375
|
-
" At the end of each session, context-vault flush confirms the vault is healthy.",
|
|
2376
|
-
),
|
|
2377
|
-
);
|
|
2378
|
-
} else {
|
|
2379
|
-
console.log(
|
|
2380
|
-
`\n ${yellow("!")} Session auto-flush hook already installed.\n`,
|
|
2410
|
+
` ${green("+")} ${name} — installed to ~/.claude/skills/${name}/`,
|
|
2381
2411
|
);
|
|
2382
2412
|
}
|
|
2383
|
-
|
|
2384
|
-
console.
|
|
2385
|
-
|
|
2386
|
-
);
|
|
2387
|
-
process.exit(1);
|
|
2388
|
-
}
|
|
2389
|
-
console.log();
|
|
2390
|
-
}
|
|
2391
|
-
} else if (sub === "remove") {
|
|
2392
|
-
try {
|
|
2393
|
-
const removed = removeClaudeHook();
|
|
2394
|
-
if (removed) {
|
|
2395
|
-
console.log(`\n ${green("✓")} Claude Code memory hook removed.\n`);
|
|
2396
|
-
} else {
|
|
2397
|
-
console.log(`\n ${yellow("!")} Hook not found — nothing to remove.\n`);
|
|
2413
|
+
console.log();
|
|
2414
|
+
console.log(dim(" Skills are active immediately in Claude Code."));
|
|
2415
|
+
console.log(dim(` Trigger with: /${names.join(", /")}`));
|
|
2398
2416
|
}
|
|
2399
2417
|
} catch (e) {
|
|
2400
|
-
console.error(
|
|
2418
|
+
console.error(` ${red("x")} Skills install failed: ${e.message}\n`);
|
|
2401
2419
|
process.exit(1);
|
|
2402
2420
|
}
|
|
2421
|
+
console.log();
|
|
2422
|
+
} else {
|
|
2423
|
+
console.log(`
|
|
2424
|
+
${bold("context-vault skills")} <install>
|
|
2425
|
+
|
|
2426
|
+
Manage bundled Claude Code skills.
|
|
2427
|
+
|
|
2428
|
+
${bold("Commands:")}
|
|
2429
|
+
${cyan("skills install")} Copy bundled skills into ~/.claude/skills/
|
|
2430
|
+
|
|
2431
|
+
${bold("Bundled skills:")}
|
|
2432
|
+
${cyan("compile-context")} Compile vault entries into a project brief using create_snapshot
|
|
2433
|
+
`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
async function runHooksInstall() {
|
|
2438
|
+
try {
|
|
2439
|
+
const installed = installClaudeHook();
|
|
2440
|
+
if (installed) {
|
|
2441
|
+
console.log(
|
|
2442
|
+
`\n ${green("✓")} Hook installed. Context vault will inject relevant entries on every prompt.\n`,
|
|
2443
|
+
);
|
|
2444
|
+
console.log(
|
|
2445
|
+
dim(
|
|
2446
|
+
" On every prompt, context-vault searches your vault for relevant entries",
|
|
2447
|
+
),
|
|
2448
|
+
);
|
|
2449
|
+
console.log(
|
|
2450
|
+
dim(
|
|
2451
|
+
" and injects them as a <context-vault> block before Claude sees your message.",
|
|
2452
|
+
),
|
|
2453
|
+
);
|
|
2454
|
+
console.log(
|
|
2455
|
+
dim(`\n To remove: ${cyan("context-vault hooks uninstall")}`),
|
|
2456
|
+
);
|
|
2457
|
+
} else {
|
|
2458
|
+
console.log(`\n ${yellow("!")} Hook already installed.\n`);
|
|
2459
|
+
}
|
|
2460
|
+
} catch (e) {
|
|
2461
|
+
console.error(`\n ${red("x")} Failed to install hook: ${e.message}\n`);
|
|
2462
|
+
process.exit(1);
|
|
2463
|
+
}
|
|
2464
|
+
console.log();
|
|
2403
2465
|
|
|
2466
|
+
const installFlush =
|
|
2467
|
+
flags.has("--flush") ||
|
|
2468
|
+
(await prompt(
|
|
2469
|
+
" Install SessionEnd flush hook? (saves vault health summary at session end) (y/N):",
|
|
2470
|
+
"n",
|
|
2471
|
+
));
|
|
2472
|
+
const shouldInstallFlush =
|
|
2473
|
+
installFlush === true ||
|
|
2474
|
+
(typeof installFlush === "string" &&
|
|
2475
|
+
installFlush.toLowerCase().startsWith("y"));
|
|
2476
|
+
|
|
2477
|
+
if (shouldInstallFlush) {
|
|
2404
2478
|
try {
|
|
2405
|
-
const
|
|
2406
|
-
if (
|
|
2479
|
+
const flushInstalled = installSessionEndHook();
|
|
2480
|
+
if (flushInstalled) {
|
|
2481
|
+
console.log(`\n ${green("✓")} SessionEnd flush hook installed.\n`);
|
|
2407
2482
|
console.log(
|
|
2408
|
-
|
|
2483
|
+
dim(
|
|
2484
|
+
" At the end of each session, context-vault flush confirms the vault is healthy.",
|
|
2485
|
+
),
|
|
2486
|
+
);
|
|
2487
|
+
} else {
|
|
2488
|
+
console.log(
|
|
2489
|
+
`\n ${yellow("!")} SessionEnd flush hook already installed.\n`,
|
|
2409
2490
|
);
|
|
2410
2491
|
}
|
|
2411
2492
|
} catch (e) {
|
|
2412
2493
|
console.error(
|
|
2413
|
-
`\n ${red("x")} Failed to
|
|
2494
|
+
`\n ${red("x")} Failed to install session flush hook: ${e.message}\n`,
|
|
2414
2495
|
);
|
|
2496
|
+
process.exit(1);
|
|
2415
2497
|
}
|
|
2498
|
+
console.log();
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
async function runHooksUninstall() {
|
|
2503
|
+
try {
|
|
2504
|
+
const removed = removeClaudeHook();
|
|
2505
|
+
if (removed) {
|
|
2506
|
+
console.log(`\n ${green("✓")} Claude Code memory hook removed.\n`);
|
|
2507
|
+
} else {
|
|
2508
|
+
console.log(`\n ${yellow("!")} Hook not found — nothing to remove.\n`);
|
|
2509
|
+
}
|
|
2510
|
+
} catch (e) {
|
|
2511
|
+
console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
|
|
2512
|
+
process.exit(1);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
try {
|
|
2516
|
+
const flushRemoved = removeSessionEndHook();
|
|
2517
|
+
if (flushRemoved) {
|
|
2518
|
+
console.log(`\n ${green("✓")} SessionEnd flush hook removed.\n`);
|
|
2519
|
+
}
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
console.error(
|
|
2522
|
+
`\n ${red("x")} Failed to remove session flush hook: ${e.message}\n`,
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
async function runHooks() {
|
|
2528
|
+
const sub = args[1];
|
|
2529
|
+
|
|
2530
|
+
if (sub === "install") {
|
|
2531
|
+
await runHooksInstall();
|
|
2532
|
+
} else if (sub === "remove" || sub === "uninstall") {
|
|
2533
|
+
await runHooksUninstall();
|
|
2416
2534
|
} else {
|
|
2417
2535
|
console.log(`
|
|
2418
|
-
${bold("context-vault hooks")} <install|
|
|
2536
|
+
${bold("context-vault hooks")} <install|uninstall>
|
|
2419
2537
|
|
|
2420
2538
|
Manage the Claude Code memory hook integration.
|
|
2421
2539
|
When installed, context-vault automatically searches your vault on every user
|
|
2422
|
-
prompt and injects relevant entries as
|
|
2540
|
+
prompt and injects relevant entries as a <context-vault> XML block.
|
|
2423
2541
|
|
|
2424
2542
|
${bold("Commands:")}
|
|
2425
|
-
${cyan("hooks install")}
|
|
2426
|
-
|
|
2427
|
-
${cyan("hooks
|
|
2543
|
+
${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2544
|
+
Also prompts to install a SessionEnd flush hook
|
|
2545
|
+
${cyan("hooks uninstall")} Remove the recall hook and SessionEnd flush hook
|
|
2546
|
+
`);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
async function runClaude() {
|
|
2551
|
+
const sub = args[1];
|
|
2552
|
+
|
|
2553
|
+
if (sub === "install") {
|
|
2554
|
+
await runHooksInstall();
|
|
2555
|
+
} else if (sub === "uninstall" || sub === "remove") {
|
|
2556
|
+
await runHooksUninstall();
|
|
2557
|
+
} else {
|
|
2558
|
+
console.log(`
|
|
2559
|
+
${bold("context-vault claude")} <install|uninstall>
|
|
2560
|
+
|
|
2561
|
+
Manage the Claude Code memory hook integration.
|
|
2562
|
+
Alias for ${cyan("context-vault hooks install|uninstall")}.
|
|
2563
|
+
|
|
2564
|
+
${bold("Commands:")}
|
|
2565
|
+
${cyan("claude install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2566
|
+
${cyan("claude uninstall")} Remove the recall hook and SessionEnd flush hook
|
|
2428
2567
|
`);
|
|
2429
2568
|
}
|
|
2430
2569
|
}
|
|
@@ -2652,6 +2791,12 @@ async function main() {
|
|
|
2652
2791
|
case "hooks":
|
|
2653
2792
|
await runHooks();
|
|
2654
2793
|
break;
|
|
2794
|
+
case "claude":
|
|
2795
|
+
await runClaude();
|
|
2796
|
+
break;
|
|
2797
|
+
case "skills":
|
|
2798
|
+
await runSkills();
|
|
2799
|
+
break;
|
|
2655
2800
|
case "flush":
|
|
2656
2801
|
await runFlush();
|
|
2657
2802
|
break;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { hybridSearch } from "../../retrieve/index.js";
|
|
3
|
+
import { captureAndIndex } from "../../capture/index.js";
|
|
4
|
+
import { normalizeKind } from "../../core/files.js";
|
|
5
|
+
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
6
|
+
|
|
7
|
+
const NOISE_KINDS = new Set(["prompt-history", "task-notification"]);
|
|
8
|
+
const SYNTHESIS_MODEL = "claude-haiku-4-5-20251001";
|
|
9
|
+
const MAX_ENTRIES_FOR_SYNTHESIS = 40;
|
|
10
|
+
const MAX_BODY_PER_ENTRY = 600;
|
|
11
|
+
|
|
12
|
+
export const name = "create_snapshot";
|
|
13
|
+
|
|
14
|
+
export const description =
|
|
15
|
+
"Pull all relevant vault entries matching a topic, run an LLM synthesis pass to deduplicate and structure them into a context brief, then save and return the brief's ULID. The brief is saved as kind: 'brief' with a deterministic identity_key for retrieval.";
|
|
16
|
+
|
|
17
|
+
export const inputSchema = {
|
|
18
|
+
topic: z.string().describe("The topic or project name to snapshot"),
|
|
19
|
+
tags: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Optional tag filters — entries must match at least one"),
|
|
23
|
+
kinds: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional kind filters to restrict which entry types are pulled"),
|
|
27
|
+
identity_key: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(
|
|
31
|
+
"Deterministic key for the saved brief (defaults to slugified topic). Use the same key to overwrite a previous snapshot.",
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function buildSynthesisPrompt(topic, entries) {
|
|
36
|
+
const entriesBlock = entries
|
|
37
|
+
.map((e, i) => {
|
|
38
|
+
const tags = e.tags ? JSON.parse(e.tags) : [];
|
|
39
|
+
const tagStr = tags.length ? tags.join(", ") : "none";
|
|
40
|
+
const body = e.body
|
|
41
|
+
? e.body.slice(0, MAX_BODY_PER_ENTRY) +
|
|
42
|
+
(e.body.length > MAX_BODY_PER_ENTRY ? "…" : "")
|
|
43
|
+
: "(no body)";
|
|
44
|
+
return [
|
|
45
|
+
`### Entry ${i + 1} [${e.kind}] id: ${e.id}`,
|
|
46
|
+
`tags: ${tagStr}`,
|
|
47
|
+
`updated: ${e.updated_at || e.created_at || "unknown"}`,
|
|
48
|
+
body,
|
|
49
|
+
].join("\n");
|
|
50
|
+
})
|
|
51
|
+
.join("\n\n");
|
|
52
|
+
|
|
53
|
+
return `You are a knowledge synthesis assistant. Given the following vault entries about "${topic}", produce a structured context brief.
|
|
54
|
+
|
|
55
|
+
Deduplicate overlapping information, resolve any contradictions (note them in Audit Notes), and organise the content into the sections below. Keep each section concise and actionable. Omit sections that have no relevant content.
|
|
56
|
+
|
|
57
|
+
Output ONLY the markdown document — no preamble, no explanation.
|
|
58
|
+
|
|
59
|
+
Required format:
|
|
60
|
+
# ${topic} — Context Brief
|
|
61
|
+
## Status
|
|
62
|
+
(current state of the topic)
|
|
63
|
+
## Key Decisions
|
|
64
|
+
(architectural or strategic decisions made)
|
|
65
|
+
## Patterns & Conventions
|
|
66
|
+
(recurring patterns, coding conventions, standards)
|
|
67
|
+
## Active Constraints
|
|
68
|
+
(known limitations, hard requirements, deadlines)
|
|
69
|
+
## Open Questions
|
|
70
|
+
(unresolved questions or areas needing investigation)
|
|
71
|
+
## Audit Notes
|
|
72
|
+
(contradictions detected, stale entries flagged with their ids)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
VAULT ENTRIES:
|
|
76
|
+
|
|
77
|
+
${entriesBlock}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function callLlm(prompt) {
|
|
81
|
+
const { Anthropic } = await import("@anthropic-ai/sdk");
|
|
82
|
+
const client = new Anthropic();
|
|
83
|
+
const message = await client.messages.create({
|
|
84
|
+
model: SYNTHESIS_MODEL,
|
|
85
|
+
max_tokens: 2048,
|
|
86
|
+
messages: [{ role: "user", content: prompt }],
|
|
87
|
+
});
|
|
88
|
+
const block = message.content.find((b) => b.type === "text");
|
|
89
|
+
if (!block) throw new Error("LLM returned no text content");
|
|
90
|
+
return block.text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slugifyTopic(topic) {
|
|
94
|
+
return topic
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
97
|
+
.replace(/^-+|-+$/g, "")
|
|
98
|
+
.slice(0, 120);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function handler(
|
|
102
|
+
{ topic, tags, kinds, identity_key },
|
|
103
|
+
ctx,
|
|
104
|
+
{ ensureIndexed },
|
|
105
|
+
) {
|
|
106
|
+
const { config } = ctx;
|
|
107
|
+
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
108
|
+
|
|
109
|
+
const vaultErr = ensureVaultExists(config);
|
|
110
|
+
if (vaultErr) return vaultErr;
|
|
111
|
+
|
|
112
|
+
if (!topic?.trim()) {
|
|
113
|
+
return err("Required: topic (non-empty string)", "INVALID_INPUT");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await ensureIndexed();
|
|
117
|
+
|
|
118
|
+
const normalizedKinds = kinds?.map(normalizeKind) ?? [];
|
|
119
|
+
|
|
120
|
+
let candidates = [];
|
|
121
|
+
|
|
122
|
+
if (normalizedKinds.length > 0) {
|
|
123
|
+
for (const kindFilter of normalizedKinds) {
|
|
124
|
+
const rows = await hybridSearch(ctx, topic, {
|
|
125
|
+
kindFilter,
|
|
126
|
+
limit: Math.ceil(MAX_ENTRIES_FOR_SYNTHESIS / normalizedKinds.length),
|
|
127
|
+
userIdFilter: userId,
|
|
128
|
+
includeSuperseeded: false,
|
|
129
|
+
});
|
|
130
|
+
candidates.push(...rows);
|
|
131
|
+
}
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
candidates = candidates.filter((r) => {
|
|
134
|
+
if (seen.has(r.id)) return false;
|
|
135
|
+
seen.add(r.id);
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
candidates = await hybridSearch(ctx, topic, {
|
|
140
|
+
limit: MAX_ENTRIES_FOR_SYNTHESIS,
|
|
141
|
+
userIdFilter: userId,
|
|
142
|
+
includeSuperseeded: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (tags?.length) {
|
|
147
|
+
candidates = candidates.filter((r) => {
|
|
148
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
149
|
+
return tags.some((t) => entryTags.includes(t));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const noiseIds = candidates
|
|
154
|
+
.filter((r) => NOISE_KINDS.has(r.kind))
|
|
155
|
+
.map((r) => r.id);
|
|
156
|
+
|
|
157
|
+
const synthesisEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
|
|
158
|
+
|
|
159
|
+
if (synthesisEntries.length === 0) {
|
|
160
|
+
return err(
|
|
161
|
+
`No entries found for topic "${topic}" to synthesize. Try a broader topic or different tags.`,
|
|
162
|
+
"NO_ENTRIES",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let briefBody;
|
|
167
|
+
try {
|
|
168
|
+
const prompt = buildSynthesisPrompt(topic, synthesisEntries);
|
|
169
|
+
briefBody = await callLlm(prompt);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return err(
|
|
172
|
+
`LLM synthesis failed: ${e.message}. Ensure ANTHROPIC_API_KEY is set.`,
|
|
173
|
+
"LLM_ERROR",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const effectiveIdentityKey =
|
|
178
|
+
identity_key ?? `snapshot-${slugifyTopic(topic)}`;
|
|
179
|
+
|
|
180
|
+
const briefTags = [
|
|
181
|
+
"snapshot",
|
|
182
|
+
...(tags ?? []),
|
|
183
|
+
...(normalizedKinds.length > 0 ? [] : []),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const supersedes = noiseIds.length > 0 ? noiseIds : undefined;
|
|
187
|
+
|
|
188
|
+
const entry = await captureAndIndex(ctx, {
|
|
189
|
+
kind: "brief",
|
|
190
|
+
title: `${topic} — Context Brief`,
|
|
191
|
+
body: briefBody,
|
|
192
|
+
tags: briefTags,
|
|
193
|
+
source: "create_snapshot",
|
|
194
|
+
identity_key: effectiveIdentityKey,
|
|
195
|
+
supersedes,
|
|
196
|
+
userId,
|
|
197
|
+
meta: {
|
|
198
|
+
topic,
|
|
199
|
+
entry_count: synthesisEntries.length,
|
|
200
|
+
noise_superseded: noiseIds.length,
|
|
201
|
+
synthesized_from: synthesisEntries.map((e) => e.id),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const parts = [
|
|
206
|
+
`✓ Snapshot created → id: ${entry.id}`,
|
|
207
|
+
` title: ${entry.title}`,
|
|
208
|
+
` identity_key: ${effectiveIdentityKey}`,
|
|
209
|
+
` synthesized from: ${synthesisEntries.length} entries`,
|
|
210
|
+
noiseIds.length > 0
|
|
211
|
+
? ` noise superseded: ${noiseIds.length} entries`
|
|
212
|
+
: null,
|
|
213
|
+
"",
|
|
214
|
+
"_Retrieve with: get_context(kind: 'brief', identity_key: '" +
|
|
215
|
+
effectiveIdentityKey +
|
|
216
|
+
"')_",
|
|
217
|
+
]
|
|
218
|
+
.filter((l) => l !== null)
|
|
219
|
+
.join("\n");
|
|
220
|
+
|
|
221
|
+
return ok(parts);
|
|
222
|
+
}
|
|
@@ -12,6 +12,7 @@ import * as submitFeedback from "./tools/submit-feedback.js";
|
|
|
12
12
|
import * as ingestUrl from "./tools/ingest-url.js";
|
|
13
13
|
import * as contextStatus from "./tools/context-status.js";
|
|
14
14
|
import * as clearContext from "./tools/clear-context.js";
|
|
15
|
+
import * as createSnapshot from "./tools/create-snapshot.js";
|
|
15
16
|
|
|
16
17
|
const toolModules = [
|
|
17
18
|
getContext,
|
|
@@ -22,6 +23,7 @@ const toolModules = [
|
|
|
22
23
|
ingestUrl,
|
|
23
24
|
contextStatus,
|
|
24
25
|
clearContext,
|
|
26
|
+
createSnapshot,
|
|
25
27
|
];
|
|
26
28
|
|
|
27
29
|
const TOOL_TIMEOUT_MS = 60_000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"prepack": "node scripts/prepack.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
+
"assets/",
|
|
20
21
|
"bin/",
|
|
21
22
|
"src/",
|
|
22
23
|
"scripts/",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"@context-vault/core"
|
|
56
57
|
],
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@context-vault/core": "^2.
|
|
59
|
+
"@context-vault/core": "^2.14.0",
|
|
59
60
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
61
|
"sqlite-vec": "^0.1.0"
|
|
61
62
|
}
|