@context-vault/core 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-vault/core",
|
|
3
|
-
"version": "2.
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
39
40
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
40
41
|
"sqlite-vec": "^0.1.0"
|
|
41
42
|
},
|
|
@@ -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
|
+
}
|
package/src/server/tools.js
CHANGED
|
@@ -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;
|