@context-vault/core 2.13.0 → 2.15.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 +2 -1
- package/src/capture/index.js +11 -0
- package/src/consolidation/index.js +112 -0
- package/src/core/categories.js +10 -0
- package/src/core/config.js +37 -0
- package/src/index/db.js +102 -9
- package/src/index/index.js +24 -1
- package/src/index.js +4 -0
- package/src/retrieve/index.js +261 -64
- package/src/server/tools/create-snapshot.js +231 -0
- package/src/server/tools/get-context.js +297 -11
- package/src/server/tools/ingest-project.js +244 -0
- package/src/server/tools/list-buckets.js +116 -0
- package/src/server/tools/save-context.js +190 -19
- package/src/server/tools/session-start.js +285 -0
- package/src/server/tools.js +8 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_TOKENS = 4000;
|
|
6
|
+
const RECENT_DAYS = 7;
|
|
7
|
+
const MAX_BODY_PER_ENTRY = 400;
|
|
8
|
+
const PRIORITY_KINDS = ["decision", "insight", "pattern"];
|
|
9
|
+
const SESSION_SUMMARY_KIND = "session";
|
|
10
|
+
|
|
11
|
+
export const name = "session_start";
|
|
12
|
+
|
|
13
|
+
export const description =
|
|
14
|
+
"Auto-assemble a context brief for the current project on session start. Pulls recent entries, last session summary, and active decisions/blockers into a token-budgeted capsule formatted for agent consumption.";
|
|
15
|
+
|
|
16
|
+
export const inputSchema = {
|
|
17
|
+
project: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
"Project name or tag to scope the brief. Auto-detected from cwd/git remote if not provided.",
|
|
22
|
+
),
|
|
23
|
+
max_tokens: z
|
|
24
|
+
.number()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(
|
|
27
|
+
"Token budget for the capsule (rough estimate: 1 token ~ 4 chars). Default: 4000.",
|
|
28
|
+
),
|
|
29
|
+
buckets: z
|
|
30
|
+
.array(z.string())
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"Bucket names to scope the session brief. Each name expands to a 'bucket:<name>' tag filter. When provided, the brief only includes entries from these buckets.",
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function detectProject() {
|
|
38
|
+
try {
|
|
39
|
+
const remote = execSync("git remote get-url origin 2>/dev/null", {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
timeout: 3000,
|
|
42
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (remote) {
|
|
45
|
+
const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
|
|
46
|
+
if (match) return match[1];
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const parts = cwd.split(/[/\\]/);
|
|
53
|
+
return parts[parts.length - 1];
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function truncateBody(body, maxLen = MAX_BODY_PER_ENTRY) {
|
|
60
|
+
if (!body) return "(no body)";
|
|
61
|
+
if (body.length <= maxLen) return body;
|
|
62
|
+
return body.slice(0, maxLen) + "...";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function estimateTokens(text) {
|
|
66
|
+
return Math.ceil((text || "").length / 4);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatEntry(entry) {
|
|
70
|
+
const tags = entry.tags ? JSON.parse(entry.tags) : [];
|
|
71
|
+
const tagStr = tags.length ? tags.join(", ") : "none";
|
|
72
|
+
const date = entry.updated_at || entry.created_at || "unknown";
|
|
73
|
+
return [
|
|
74
|
+
`- **${entry.title || "(untitled)"}** [${entry.kind}]`,
|
|
75
|
+
` tags: ${tagStr} | ${date} | id: \`${entry.id}\``,
|
|
76
|
+
` ${truncateBody(entry.body).replace(/\n+/g, " ").trim()}`,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function handler({ project, max_tokens, buckets }, ctx, { ensureIndexed }) {
|
|
81
|
+
const { config } = ctx;
|
|
82
|
+
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
83
|
+
|
|
84
|
+
const vaultErr = ensureVaultExists(config);
|
|
85
|
+
if (vaultErr) return vaultErr;
|
|
86
|
+
|
|
87
|
+
await ensureIndexed();
|
|
88
|
+
|
|
89
|
+
const effectiveProject = project?.trim() || detectProject();
|
|
90
|
+
const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
|
|
91
|
+
|
|
92
|
+
const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
|
|
93
|
+
const effectiveTags = bucketTags.length
|
|
94
|
+
? bucketTags
|
|
95
|
+
: effectiveProject
|
|
96
|
+
? [effectiveProject]
|
|
97
|
+
: [];
|
|
98
|
+
|
|
99
|
+
const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
|
|
100
|
+
|
|
101
|
+
const sections = [];
|
|
102
|
+
let tokensUsed = 0;
|
|
103
|
+
|
|
104
|
+
sections.push(
|
|
105
|
+
`# Session Brief${effectiveProject ? ` — ${effectiveProject}` : ""}`,
|
|
106
|
+
);
|
|
107
|
+
const bucketsLabel = buckets?.length ? ` | buckets: ${buckets.join(", ")}` : "";
|
|
108
|
+
sections.push(
|
|
109
|
+
`_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}_\n`,
|
|
110
|
+
);
|
|
111
|
+
tokensUsed += estimateTokens(sections.join("\n"));
|
|
112
|
+
|
|
113
|
+
const lastSession = queryLastSession(ctx, userId, effectiveTags);
|
|
114
|
+
if (lastSession) {
|
|
115
|
+
const sessionBlock = [
|
|
116
|
+
"## Last Session Summary",
|
|
117
|
+
truncateBody(lastSession.body, 600),
|
|
118
|
+
"",
|
|
119
|
+
].join("\n");
|
|
120
|
+
const sessionTokens = estimateTokens(sessionBlock);
|
|
121
|
+
if (tokensUsed + sessionTokens <= tokenBudget) {
|
|
122
|
+
sections.push(sessionBlock);
|
|
123
|
+
tokensUsed += sessionTokens;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const decisions = queryByKinds(
|
|
128
|
+
ctx,
|
|
129
|
+
PRIORITY_KINDS,
|
|
130
|
+
sinceDate,
|
|
131
|
+
userId,
|
|
132
|
+
effectiveTags,
|
|
133
|
+
);
|
|
134
|
+
if (decisions.length > 0) {
|
|
135
|
+
const header = "## Active Decisions, Insights & Patterns\n";
|
|
136
|
+
const headerTokens = estimateTokens(header);
|
|
137
|
+
if (tokensUsed + headerTokens <= tokenBudget) {
|
|
138
|
+
const entryLines = [];
|
|
139
|
+
tokensUsed += headerTokens;
|
|
140
|
+
for (const entry of decisions) {
|
|
141
|
+
const line = formatEntry(entry);
|
|
142
|
+
const lineTokens = estimateTokens(line);
|
|
143
|
+
if (tokensUsed + lineTokens > tokenBudget) break;
|
|
144
|
+
entryLines.push(line);
|
|
145
|
+
tokensUsed += lineTokens;
|
|
146
|
+
}
|
|
147
|
+
if (entryLines.length > 0) {
|
|
148
|
+
sections.push(header + entryLines.join("\n") + "\n");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const recent = queryRecent(ctx, sinceDate, userId, effectiveTags);
|
|
154
|
+
const seenIds = new Set(decisions.map((d) => d.id));
|
|
155
|
+
if (lastSession) seenIds.add(lastSession.id);
|
|
156
|
+
const deduped = recent.filter((r) => !seenIds.has(r.id));
|
|
157
|
+
|
|
158
|
+
if (deduped.length > 0) {
|
|
159
|
+
const header = `## Recent Entries (last ${RECENT_DAYS} days)\n`;
|
|
160
|
+
const headerTokens = estimateTokens(header);
|
|
161
|
+
if (tokensUsed + headerTokens <= tokenBudget) {
|
|
162
|
+
const entryLines = [];
|
|
163
|
+
tokensUsed += headerTokens;
|
|
164
|
+
for (const entry of deduped) {
|
|
165
|
+
const line = formatEntry(entry);
|
|
166
|
+
const lineTokens = estimateTokens(line);
|
|
167
|
+
if (tokensUsed + lineTokens > tokenBudget) break;
|
|
168
|
+
entryLines.push(line);
|
|
169
|
+
tokensUsed += lineTokens;
|
|
170
|
+
}
|
|
171
|
+
if (entryLines.length > 0) {
|
|
172
|
+
sections.push(header + entryLines.join("\n") + "\n");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const totalEntries =
|
|
178
|
+
(lastSession ? 1 : 0) +
|
|
179
|
+
decisions.length +
|
|
180
|
+
deduped.filter((d) => {
|
|
181
|
+
const line = formatEntry(d);
|
|
182
|
+
return true;
|
|
183
|
+
}).length;
|
|
184
|
+
|
|
185
|
+
sections.push("---");
|
|
186
|
+
sections.push(
|
|
187
|
+
`_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || "unscoped"}_`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const result = ok(sections.join("\n"));
|
|
191
|
+
result._meta = {
|
|
192
|
+
project: effectiveProject || null,
|
|
193
|
+
buckets: buckets || null,
|
|
194
|
+
tokens_used: tokensUsed,
|
|
195
|
+
tokens_budget: tokenBudget,
|
|
196
|
+
sections: {
|
|
197
|
+
last_session: lastSession ? 1 : 0,
|
|
198
|
+
decisions: decisions.length,
|
|
199
|
+
recent: deduped.length,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function queryLastSession(ctx, userId, effectiveTags) {
|
|
206
|
+
const clauses = [`kind = '${SESSION_SUMMARY_KIND}'`];
|
|
207
|
+
const params = [];
|
|
208
|
+
|
|
209
|
+
if (userId !== undefined) {
|
|
210
|
+
clauses.push("user_id = ?");
|
|
211
|
+
params.push(userId);
|
|
212
|
+
}
|
|
213
|
+
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
214
|
+
clauses.push("superseded_by IS NULL");
|
|
215
|
+
|
|
216
|
+
const where = `WHERE ${clauses.join(" AND ")}`;
|
|
217
|
+
const rows = ctx.db
|
|
218
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 5`)
|
|
219
|
+
.all(...params);
|
|
220
|
+
|
|
221
|
+
if (effectiveTags.length) {
|
|
222
|
+
const match = rows.find((r) => {
|
|
223
|
+
const tags = r.tags ? JSON.parse(r.tags) : [];
|
|
224
|
+
return effectiveTags.some((t) => tags.includes(t));
|
|
225
|
+
});
|
|
226
|
+
if (match) return match;
|
|
227
|
+
}
|
|
228
|
+
return rows[0] || null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function queryByKinds(ctx, kinds, since, userId, effectiveTags) {
|
|
232
|
+
const kindPlaceholders = kinds.map(() => "?").join(",");
|
|
233
|
+
const clauses = [`kind IN (${kindPlaceholders})`];
|
|
234
|
+
const params = [...kinds];
|
|
235
|
+
|
|
236
|
+
clauses.push("created_at >= ?");
|
|
237
|
+
params.push(since);
|
|
238
|
+
|
|
239
|
+
if (userId !== undefined) {
|
|
240
|
+
clauses.push("user_id = ?");
|
|
241
|
+
params.push(userId);
|
|
242
|
+
}
|
|
243
|
+
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
244
|
+
clauses.push("superseded_by IS NULL");
|
|
245
|
+
|
|
246
|
+
const where = `WHERE ${clauses.join(" AND ")}`;
|
|
247
|
+
const rows = ctx.db
|
|
248
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
|
|
249
|
+
.all(...params);
|
|
250
|
+
|
|
251
|
+
if (effectiveTags.length) {
|
|
252
|
+
const tagged = rows.filter((r) => {
|
|
253
|
+
const tags = r.tags ? JSON.parse(r.tags) : [];
|
|
254
|
+
return effectiveTags.some((t) => tags.includes(t));
|
|
255
|
+
});
|
|
256
|
+
if (tagged.length > 0) return tagged;
|
|
257
|
+
}
|
|
258
|
+
return rows;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function queryRecent(ctx, since, userId, effectiveTags) {
|
|
262
|
+
const clauses = ["created_at >= ?"];
|
|
263
|
+
const params = [since];
|
|
264
|
+
|
|
265
|
+
if (userId !== undefined) {
|
|
266
|
+
clauses.push("user_id = ?");
|
|
267
|
+
params.push(userId);
|
|
268
|
+
}
|
|
269
|
+
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
270
|
+
clauses.push("superseded_by IS NULL");
|
|
271
|
+
|
|
272
|
+
const where = `WHERE ${clauses.join(" AND ")}`;
|
|
273
|
+
const rows = ctx.db
|
|
274
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
|
|
275
|
+
.all(...params);
|
|
276
|
+
|
|
277
|
+
if (effectiveTags.length) {
|
|
278
|
+
const tagged = rows.filter((r) => {
|
|
279
|
+
const tags = r.tags ? JSON.parse(r.tags) : [];
|
|
280
|
+
return effectiveTags.some((t) => tags.includes(t));
|
|
281
|
+
});
|
|
282
|
+
if (tagged.length > 0) return tagged;
|
|
283
|
+
}
|
|
284
|
+
return rows;
|
|
285
|
+
}
|
package/src/server/tools.js
CHANGED
|
@@ -12,6 +12,10 @@ 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";
|
|
16
|
+
import * as sessionStart from "./tools/session-start.js";
|
|
17
|
+
import * as listBuckets from "./tools/list-buckets.js";
|
|
18
|
+
import * as ingestProject from "./tools/ingest-project.js";
|
|
15
19
|
|
|
16
20
|
const toolModules = [
|
|
17
21
|
getContext,
|
|
@@ -20,8 +24,12 @@ const toolModules = [
|
|
|
20
24
|
deleteContext,
|
|
21
25
|
submitFeedback,
|
|
22
26
|
ingestUrl,
|
|
27
|
+
ingestProject,
|
|
23
28
|
contextStatus,
|
|
24
29
|
clearContext,
|
|
30
|
+
createSnapshot,
|
|
31
|
+
sessionStart,
|
|
32
|
+
listBuckets,
|
|
25
33
|
];
|
|
26
34
|
|
|
27
35
|
const TOOL_TIMEOUT_MS = 60_000;
|