@context-vault/core 2.17.0 → 3.0.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.
Files changed (101) hide show
  1. package/dist/capture.d.ts +21 -0
  2. package/dist/capture.d.ts.map +1 -0
  3. package/dist/capture.js +269 -0
  4. package/dist/capture.js.map +1 -0
  5. package/dist/categories.d.ts +6 -0
  6. package/dist/categories.d.ts.map +1 -0
  7. package/dist/categories.js +50 -0
  8. package/dist/categories.js.map +1 -0
  9. package/dist/config.d.ts +4 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +190 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/constants.d.ts +33 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +23 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/db.d.ts +13 -0
  18. package/dist/db.d.ts.map +1 -0
  19. package/dist/db.js +191 -0
  20. package/dist/db.js.map +1 -0
  21. package/dist/embed.d.ts +5 -0
  22. package/dist/embed.d.ts.map +1 -0
  23. package/dist/embed.js +78 -0
  24. package/dist/embed.js.map +1 -0
  25. package/dist/files.d.ts +13 -0
  26. package/dist/files.d.ts.map +1 -0
  27. package/dist/files.js +66 -0
  28. package/dist/files.js.map +1 -0
  29. package/dist/formatters.d.ts +8 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +18 -0
  32. package/dist/formatters.js.map +1 -0
  33. package/dist/frontmatter.d.ts +12 -0
  34. package/dist/frontmatter.d.ts.map +1 -0
  35. package/dist/frontmatter.js +101 -0
  36. package/dist/frontmatter.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +297 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/ingest-url.d.ts +20 -0
  42. package/dist/ingest-url.d.ts.map +1 -0
  43. package/dist/ingest-url.js +113 -0
  44. package/dist/ingest-url.js.map +1 -0
  45. package/dist/main.d.ts +14 -0
  46. package/dist/main.d.ts.map +1 -0
  47. package/dist/main.js +25 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/search.d.ts +18 -0
  50. package/dist/search.d.ts.map +1 -0
  51. package/dist/search.js +238 -0
  52. package/dist/search.js.map +1 -0
  53. package/dist/types.d.ts +176 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +66 -17
  58. package/src/capture.ts +308 -0
  59. package/src/categories.ts +54 -0
  60. package/src/{core/config.js → config.ts} +34 -33
  61. package/src/{constants.js → constants.ts} +6 -3
  62. package/src/db.ts +229 -0
  63. package/src/{index/embed.js → embed.ts} +10 -35
  64. package/src/files.ts +80 -0
  65. package/src/{capture/formatters.js → formatters.ts} +13 -11
  66. package/src/{core/frontmatter.js → frontmatter.ts} +27 -33
  67. package/src/index.ts +351 -0
  68. package/src/ingest-url.ts +99 -0
  69. package/src/main.ts +111 -0
  70. package/src/search.ts +285 -0
  71. package/src/types.ts +166 -0
  72. package/src/capture/file-ops.js +0 -97
  73. package/src/capture/import-pipeline.js +0 -46
  74. package/src/capture/importers.js +0 -387
  75. package/src/capture/index.js +0 -236
  76. package/src/capture/ingest-url.js +0 -252
  77. package/src/consolidation/index.js +0 -112
  78. package/src/core/categories.js +0 -72
  79. package/src/core/error-log.js +0 -54
  80. package/src/core/files.js +0 -108
  81. package/src/core/status.js +0 -350
  82. package/src/core/telemetry.js +0 -90
  83. package/src/index/db.js +0 -416
  84. package/src/index/index.js +0 -522
  85. package/src/index.js +0 -66
  86. package/src/retrieve/index.js +0 -500
  87. package/src/server/helpers.js +0 -44
  88. package/src/server/tools/clear-context.js +0 -47
  89. package/src/server/tools/context-status.js +0 -182
  90. package/src/server/tools/create-snapshot.js +0 -231
  91. package/src/server/tools/delete-context.js +0 -60
  92. package/src/server/tools/get-context.js +0 -678
  93. package/src/server/tools/ingest-project.js +0 -244
  94. package/src/server/tools/ingest-url.js +0 -88
  95. package/src/server/tools/list-buckets.js +0 -116
  96. package/src/server/tools/list-context.js +0 -163
  97. package/src/server/tools/save-context.js +0 -609
  98. package/src/server/tools/session-start.js +0 -285
  99. package/src/server/tools/submit-feedback.js +0 -55
  100. package/src/server/tools.js +0 -174
  101. package/src/sync/sync.js +0 -235
@@ -1,285 +0,0 @@
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
- }
@@ -1,55 +0,0 @@
1
- import { z } from "zod";
2
- import { captureAndIndex } from "../../capture/index.js";
3
- import { ok, ensureVaultExists } from "../helpers.js";
4
-
5
- export const name = "submit_feedback";
6
-
7
- export const description =
8
- "Report a bug, request a feature, or suggest an improvement. Feedback is stored in the vault and triaged by the development pipeline.";
9
-
10
- export const inputSchema = {
11
- type: z.enum(["bug", "feature", "improvement"]).describe("Type of feedback"),
12
- title: z.string().describe("Short summary of the feedback"),
13
- body: z.string().describe("Detailed description"),
14
- severity: z
15
- .enum(["low", "medium", "high"])
16
- .optional()
17
- .describe("Severity level (default: medium)"),
18
- };
19
-
20
- /**
21
- * @param {object} args
22
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
23
- * @param {import('../types.js').ToolShared} shared
24
- */
25
- export async function handler(
26
- { type, title, body, severity },
27
- ctx,
28
- { ensureIndexed },
29
- ) {
30
- const { config } = ctx;
31
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
32
-
33
- const vaultErr = ensureVaultExists(config);
34
- if (vaultErr) return vaultErr;
35
-
36
- await ensureIndexed();
37
-
38
- const effectiveSeverity = severity || "medium";
39
- const entry = await captureAndIndex(ctx, {
40
- kind: "feedback",
41
- title,
42
- body,
43
- tags: [type, effectiveSeverity],
44
- source: "submit_feedback",
45
- meta: { feedback_type: type, severity: effectiveSeverity, status: "new" },
46
- userId,
47
- });
48
-
49
- const relPath = entry.filePath
50
- ? entry.filePath.replace(config.vaultDir + "/", "")
51
- : entry.filePath;
52
- return ok(
53
- `Feedback submitted: ${type} [${effectiveSeverity}] → ${relPath}\n id: ${entry.id}\n title: ${title}`,
54
- );
55
- }
@@ -1,174 +0,0 @@
1
- import { reindex } from "../index/index.js";
2
- import { captureAndIndex } from "../capture/index.js";
3
- import { err } from "./helpers.js";
4
- import { sendTelemetryEvent } from "../core/telemetry.js";
5
- import pkg from "../../package.json" with { type: "json" };
6
-
7
- import * as getContext from "./tools/get-context.js";
8
- import * as saveContext from "./tools/save-context.js";
9
- import * as listContext from "./tools/list-context.js";
10
- import * as deleteContext from "./tools/delete-context.js";
11
- import * as submitFeedback from "./tools/submit-feedback.js";
12
- import * as ingestUrl from "./tools/ingest-url.js";
13
- import * as contextStatus from "./tools/context-status.js";
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";
19
-
20
- const toolModules = [
21
- getContext,
22
- saveContext,
23
- listContext,
24
- deleteContext,
25
- submitFeedback,
26
- ingestUrl,
27
- ingestProject,
28
- contextStatus,
29
- clearContext,
30
- createSnapshot,
31
- sessionStart,
32
- listBuckets,
33
- ];
34
-
35
- const TOOL_TIMEOUT_MS = 60_000;
36
-
37
- export function registerTools(server, ctx) {
38
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
39
-
40
- function tracked(handler, toolName) {
41
- return async (...args) => {
42
- if (ctx.activeOps) ctx.activeOps.count++;
43
- let timer;
44
- let handlerPromise;
45
- try {
46
- handlerPromise = Promise.resolve(handler(...args));
47
- const result = await Promise.race([
48
- handlerPromise,
49
- new Promise((_, reject) => {
50
- timer = setTimeout(
51
- () => reject(new Error("TOOL_TIMEOUT")),
52
- TOOL_TIMEOUT_MS,
53
- );
54
- }),
55
- ]);
56
- if (ctx.toolStats) ctx.toolStats.ok++;
57
- return result;
58
- } catch (e) {
59
- if (e.message === "TOOL_TIMEOUT") {
60
- // Suppress any late rejection from the still-running handler to
61
- // prevent unhandled promise rejection warnings in the host process.
62
- handlerPromise?.catch(() => {});
63
- if (ctx.toolStats) {
64
- ctx.toolStats.errors++;
65
- ctx.toolStats.lastError = {
66
- tool: toolName,
67
- code: "TIMEOUT",
68
- timestamp: Date.now(),
69
- };
70
- }
71
- sendTelemetryEvent(ctx.config, {
72
- event: "tool_error",
73
- code: "TIMEOUT",
74
- tool: toolName,
75
- cv_version: pkg.version,
76
- });
77
- return err(
78
- "Tool timed out after 60s. Try a simpler query or run `context-vault reindex` first.",
79
- "TIMEOUT",
80
- );
81
- }
82
- if (ctx.toolStats) {
83
- ctx.toolStats.errors++;
84
- ctx.toolStats.lastError = {
85
- tool: toolName,
86
- code: "UNKNOWN",
87
- timestamp: Date.now(),
88
- };
89
- }
90
- sendTelemetryEvent(ctx.config, {
91
- event: "tool_error",
92
- code: "UNKNOWN",
93
- tool: toolName,
94
- cv_version: pkg.version,
95
- });
96
- try {
97
- await captureAndIndex(ctx, {
98
- kind: "feedback",
99
- title: `Unhandled error in ${toolName ?? "tool"} call`,
100
- body: `${e.message}\n\n${e.stack ?? ""}`,
101
- tags: ["bug", "auto-captured"],
102
- source: "auto-capture",
103
- meta: {
104
- tool: toolName,
105
- error_type: e.constructor?.name,
106
- cv_version: pkg.version,
107
- auto: true,
108
- },
109
- });
110
- } catch {} // never block on feedback capture
111
- throw e;
112
- } finally {
113
- clearTimeout(timer);
114
- if (ctx.activeOps) ctx.activeOps.count--;
115
- }
116
- };
117
- }
118
-
119
- // In hosted mode, skip reindex — DB is always in sync via writeEntry→indexEntry
120
- let reindexDone = userId !== undefined ? true : false;
121
- let reindexPromise = null;
122
- let reindexAttempts = 0;
123
- let reindexFailed = false;
124
- const MAX_REINDEX_ATTEMPTS = 2;
125
-
126
- async function ensureIndexed() {
127
- if (reindexDone) return;
128
- if (reindexPromise) return reindexPromise;
129
- // Assign promise synchronously to prevent concurrent calls from both entering reindex()
130
- const promise = reindex(ctx, { fullSync: true })
131
- .then((stats) => {
132
- reindexDone = true;
133
- const total = stats.added + stats.updated + stats.removed;
134
- if (total > 0) {
135
- console.error(
136
- `[context-vault] Auto-reindex: +${stats.added} ~${stats.updated} -${stats.removed} (${stats.unchanged} unchanged)`,
137
- );
138
- }
139
- })
140
- .catch((e) => {
141
- reindexAttempts++;
142
- console.error(
143
- `[context-vault] Auto-reindex failed (attempt ${reindexAttempts}/${MAX_REINDEX_ATTEMPTS}): ${e.message}`,
144
- );
145
- if (reindexAttempts >= MAX_REINDEX_ATTEMPTS) {
146
- console.error(
147
- `[context-vault] Giving up on auto-reindex. Run \`context-vault reindex\` manually to diagnose.`,
148
- );
149
- reindexDone = true;
150
- reindexFailed = true;
151
- } else {
152
- reindexPromise = null; // Allow retry on next tool call
153
- }
154
- });
155
- reindexPromise = promise;
156
- return reindexPromise;
157
- }
158
-
159
- const shared = {
160
- ensureIndexed,
161
- get reindexFailed() {
162
- return reindexFailed;
163
- },
164
- };
165
-
166
- for (const mod of toolModules) {
167
- server.tool(
168
- mod.name,
169
- mod.description,
170
- mod.inputSchema,
171
- tracked((args) => mod.handler(args, ctx, shared), mod.name),
172
- );
173
- }
174
- }