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.
@@ -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|remove Install or remove Claude Code memory hook
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 lines = ["## Context Vault\n"];
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
- lines.push(`### ${r.title || "(untitled)"} [${r.kind}]`);
2133
- if (entryTags.length) lines.push(`tags: ${entryTags.join(", ")}`);
2134
- lines.push(r.body?.slice(0, 400) + (r.body?.length > 400 ? "..." : ""));
2135
- lines.push("");
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
- process.stdout.write(lines.join("\n"));
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 runHooks() {
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 installed = installClaudeHook();
2330
- if (installed) {
2331
- console.log(`\n ${green("")} Claude Code memory hook installed.\n`);
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
- console.log(`\n ${yellow("!")} Hook already installed.\n`);
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
- dim(
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
- } catch (e) {
2384
- console.error(
2385
- `\n ${red("x")} Failed to install session flush hook: ${e.message}\n`,
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(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
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 flushRemoved = removeSessionEndHook();
2406
- if (flushRemoved) {
2479
+ const flushInstalled = installSessionEndHook();
2480
+ if (flushInstalled) {
2481
+ console.log(`\n ${green("✓")} SessionEnd flush hook installed.\n`);
2407
2482
  console.log(
2408
- `\n ${green("✓")} Session auto-flush hook removed (SessionEnd).\n`,
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 remove session flush hook: ${e.message}\n`,
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|remove>
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 additional context.
2540
+ prompt and injects relevant entries as a <context-vault> XML block.
2423
2541
 
2424
2542
  ${bold("Commands:")}
2425
- ${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
2426
- Also prompts to install a SessionEnd auto-flush hook
2427
- ${cyan("hooks remove")} Remove the recall hook and SessionEnd flush hook
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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -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.13.0",
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.13.0",
59
+ "@context-vault/core": "^2.14.0",
59
60
  "@modelcontextprotocol/sdk": "^1.26.0",
60
61
  "sqlite-vec": "^0.1.0"
61
62
  }