@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,182 +0,0 @@
1
- import { gatherVaultStatus, computeGrowthWarnings } from "../../core/status.js";
2
- import { errorLogPath, errorLogCount } from "../../core/error-log.js";
3
- import { ok } from "../helpers.js";
4
-
5
- function relativeTime(ts) {
6
- const secs = Math.floor((Date.now() - ts) / 1000);
7
- if (secs < 60) return `${secs}s ago`;
8
- const mins = Math.floor(secs / 60);
9
- if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"} ago`;
10
- const hrs = Math.floor(mins / 60);
11
- return `${hrs} hour${hrs === 1 ? "" : "s"} ago`;
12
- }
13
-
14
- export const name = "context_status";
15
-
16
- export const description =
17
- "Show vault health: resolved config, file counts per kind, database size, and any issues. Use to verify setup or troubleshoot. Call this when a user asks about their vault or to debug search issues.";
18
-
19
- export const inputSchema = {};
20
-
21
- /**
22
- * @param {object} _args
23
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
24
- */
25
- export function handler(_args, ctx) {
26
- const { config } = ctx;
27
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
28
-
29
- const status = gatherVaultStatus(ctx, { userId });
30
-
31
- const hasIssues = status.stalePaths || status.embeddingStatus?.missing > 0;
32
- const healthIcon = hasIssues ? "⚠" : "✓";
33
-
34
- const lines = [
35
- `## ${healthIcon} Vault Status (connected)`,
36
- ``,
37
- `Vault: ${config.vaultDir} (${config.vaultDirExists ? status.fileCount + " files" : "missing"})`,
38
- `Database: ${config.dbPath} (${status.dbSize})`,
39
- `Dev dir: ${config.devDir}`,
40
- `Data dir: ${config.dataDir}`,
41
- `Config: ${config.configPath}`,
42
- `Resolved via: ${status.resolvedFrom}`,
43
- `Schema: v9 (updated_at, superseded_by)`,
44
- ];
45
-
46
- if (status.embeddingStatus) {
47
- const { indexed, total, missing } = status.embeddingStatus;
48
- const pct = total > 0 ? Math.round((indexed / total) * 100) : 100;
49
- lines.push(`Embeddings: ${indexed}/${total} (${pct}%)`);
50
- }
51
- if (status.embedModelAvailable === false) {
52
- lines.push(
53
- `Embed model: unavailable (semantic search disabled, FTS still works)`,
54
- );
55
- } else if (status.embedModelAvailable === true) {
56
- lines.push(`Embed model: loaded`);
57
- }
58
- lines.push(`Decay: ${config.eventDecayDays} days (event recency window)`);
59
- if (status.expiredCount > 0) {
60
- lines.push(
61
- `Expired: ${status.expiredCount} entries pending prune (run \`context-vault prune\` to remove now)`,
62
- );
63
- }
64
-
65
- lines.push(``, `### Indexed`);
66
-
67
- if (status.kindCounts.length) {
68
- for (const { kind, c } of status.kindCounts) lines.push(`- ${c} ${kind}s`);
69
- } else {
70
- lines.push(`- (empty)`);
71
- }
72
-
73
- if (status.categoryCounts.length) {
74
- lines.push(``);
75
- lines.push(`### Categories`);
76
- for (const { category, c } of status.categoryCounts)
77
- lines.push(`- ${category}: ${c}`);
78
- }
79
-
80
- if (status.subdirs.length) {
81
- lines.push(``);
82
- lines.push(`### Disk Directories`);
83
- for (const { name, count } of status.subdirs)
84
- lines.push(`- ${name}/: ${count} files`);
85
- }
86
-
87
- if (status.stalePaths) {
88
- lines.push(``);
89
- lines.push(`### ⚠ Stale Paths`);
90
- lines.push(
91
- `DB contains ${status.staleCount} paths not matching current vault dir.`,
92
- );
93
- lines.push(`Auto-reindex will fix this on next search or save.`);
94
- }
95
-
96
- if (status.staleKnowledge?.length > 0) {
97
- lines.push(``);
98
- lines.push(`### ⚠ Potentially Stale Knowledge`);
99
- lines.push(
100
- `Not updated within kind staleness window (pattern: 180d, decision: 365d, reference: 90d):`,
101
- );
102
- for (const entry of status.staleKnowledge) {
103
- const lastUpdated = entry.last_updated
104
- ? entry.last_updated.split("T")[0]
105
- : "unknown";
106
- lines.push(
107
- `- "${entry.title}" (${entry.kind}) — last updated ${lastUpdated}`,
108
- );
109
- }
110
- lines.push(
111
- `Use save_context to refresh or add expires_at to retire stale entries.`,
112
- );
113
- }
114
-
115
- // Error log
116
- const logPath = errorLogPath(config.dataDir);
117
- const logCount = errorLogCount(config.dataDir);
118
- if (logCount > 0) {
119
- lines.push(``, `### Startup Error Log`);
120
- lines.push(`- Path: ${logPath}`);
121
- lines.push(`- Entries: ${logCount} (share this file for support)`);
122
- }
123
-
124
- // Health: session-level tool call stats
125
- const ts = ctx.toolStats;
126
- if (ts) {
127
- lines.push(``, `### Health`);
128
- lines.push(`- Tool calls (session): ${ts.ok} ok, ${ts.errors} errors`);
129
- if (ts.lastError) {
130
- const { tool, code, timestamp } = ts.lastError;
131
- lines.push(
132
- `- Last error: ${tool ?? "unknown"} — ${code} (${relativeTime(timestamp)})`,
133
- );
134
- }
135
- if (status.autoCapturedFeedbackCount > 0) {
136
- lines.push(
137
- `- Auto-captured feedback entries: ${status.autoCapturedFeedbackCount} (run get_context with kind:feedback tags:auto-captured)`,
138
- );
139
- }
140
- }
141
-
142
- // Growth warnings
143
- const growth = computeGrowthWarnings(status, config.thresholds);
144
- if (growth.hasWarnings) {
145
- lines.push("", "### ⚠ Vault Growth Warning");
146
- for (const w of growth.warnings) {
147
- lines.push(` ${w.message}`);
148
- }
149
- if (growth.kindBreakdown.length) {
150
- lines.push("");
151
- lines.push(" Breakdown by kind:");
152
- for (const { kind, count, pct } of growth.kindBreakdown) {
153
- lines.push(` ${kind}: ${count.toLocaleString()} (${pct}%)`);
154
- }
155
- }
156
- if (growth.actions.length) {
157
- lines.push("", "Suggested growth actions:");
158
- for (const a of growth.actions) {
159
- lines.push(` • ${a}`);
160
- }
161
- }
162
- }
163
-
164
- // Suggested actions
165
- const actions = [];
166
- if (status.stalePaths)
167
- actions.push("- Run `context-vault reindex` to fix stale paths");
168
- if (status.embeddingStatus?.missing > 0)
169
- actions.push(
170
- "- Run `context-vault reindex` to generate missing embeddings",
171
- );
172
- if (!config.vaultDirExists)
173
- actions.push("- Run `context-vault setup` to create the vault directory");
174
- if (status.kindCounts.length === 0 && config.vaultDirExists)
175
- actions.push("- Use `save_context` to add your first entry");
176
-
177
- if (actions.length) {
178
- lines.push("", "### Suggested Actions", ...actions);
179
- }
180
-
181
- return ok(lines.join("\n"));
182
- }
@@ -1,231 +0,0 @@
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
- buckets: z
24
- .array(z.string())
25
- .optional()
26
- .describe(
27
- "Filter by project-scoped buckets. Each name expands to a 'bucket:<name>' tag. Composes with 'tags' via OR (entries matching any tag or any bucket are included).",
28
- ),
29
- kinds: z
30
- .array(z.string())
31
- .optional()
32
- .describe("Optional kind filters to restrict which entry types are pulled"),
33
- identity_key: z
34
- .string()
35
- .optional()
36
- .describe(
37
- "Deterministic key for the saved brief (defaults to slugified topic). Use the same key to overwrite a previous snapshot.",
38
- ),
39
- };
40
-
41
- function buildSynthesisPrompt(topic, entries) {
42
- const entriesBlock = entries
43
- .map((e, i) => {
44
- const tags = e.tags ? JSON.parse(e.tags) : [];
45
- const tagStr = tags.length ? tags.join(", ") : "none";
46
- const body = e.body
47
- ? e.body.slice(0, MAX_BODY_PER_ENTRY) +
48
- (e.body.length > MAX_BODY_PER_ENTRY ? "…" : "")
49
- : "(no body)";
50
- return [
51
- `### Entry ${i + 1} [${e.kind}] id: ${e.id}`,
52
- `tags: ${tagStr}`,
53
- `updated: ${e.updated_at || e.created_at || "unknown"}`,
54
- body,
55
- ].join("\n");
56
- })
57
- .join("\n\n");
58
-
59
- return `You are a knowledge synthesis assistant. Given the following vault entries about "${topic}", produce a structured context brief.
60
-
61
- 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.
62
-
63
- Output ONLY the markdown document — no preamble, no explanation.
64
-
65
- Required format:
66
- # ${topic} — Context Brief
67
- ## Status
68
- (current state of the topic)
69
- ## Key Decisions
70
- (architectural or strategic decisions made)
71
- ## Patterns & Conventions
72
- (recurring patterns, coding conventions, standards)
73
- ## Active Constraints
74
- (known limitations, hard requirements, deadlines)
75
- ## Open Questions
76
- (unresolved questions or areas needing investigation)
77
- ## Audit Notes
78
- (contradictions detected, stale entries flagged with their ids)
79
-
80
- ---
81
- VAULT ENTRIES:
82
-
83
- ${entriesBlock}`;
84
- }
85
-
86
- async function callLlm(prompt) {
87
- const { Anthropic } = await import("@anthropic-ai/sdk");
88
- const client = new Anthropic();
89
- const message = await client.messages.create({
90
- model: SYNTHESIS_MODEL,
91
- max_tokens: 2048,
92
- messages: [{ role: "user", content: prompt }],
93
- });
94
- const block = message.content.find((b) => b.type === "text");
95
- if (!block) throw new Error("LLM returned no text content");
96
- return block.text;
97
- }
98
-
99
- function slugifyTopic(topic) {
100
- return topic
101
- .toLowerCase()
102
- .replace(/[^a-z0-9]+/g, "-")
103
- .replace(/^-+|-+$/g, "")
104
- .slice(0, 120);
105
- }
106
-
107
- export async function handler(
108
- { topic, tags, buckets, kinds, identity_key },
109
- ctx,
110
- { ensureIndexed },
111
- ) {
112
- const { config } = ctx;
113
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
114
-
115
- const vaultErr = ensureVaultExists(config);
116
- if (vaultErr) return vaultErr;
117
-
118
- if (!topic?.trim()) {
119
- return err("Required: topic (non-empty string)", "INVALID_INPUT");
120
- }
121
-
122
- await ensureIndexed();
123
-
124
- const normalizedKinds = kinds?.map(normalizeKind) ?? [];
125
- // Expand buckets to bucket: prefixed tags and merge with explicit tags
126
- const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
127
- const effectiveTags = [...(tags ?? []), ...bucketTags];
128
-
129
- let candidates = [];
130
-
131
- if (normalizedKinds.length > 0) {
132
- for (const kindFilter of normalizedKinds) {
133
- const rows = await hybridSearch(ctx, topic, {
134
- kindFilter,
135
- limit: Math.ceil(MAX_ENTRIES_FOR_SYNTHESIS / normalizedKinds.length),
136
- userIdFilter: userId,
137
- includeSuperseeded: false,
138
- });
139
- candidates.push(...rows);
140
- }
141
- const seen = new Set();
142
- candidates = candidates.filter((r) => {
143
- if (seen.has(r.id)) return false;
144
- seen.add(r.id);
145
- return true;
146
- });
147
- } else {
148
- candidates = await hybridSearch(ctx, topic, {
149
- limit: MAX_ENTRIES_FOR_SYNTHESIS,
150
- userIdFilter: userId,
151
- includeSuperseeded: false,
152
- });
153
- }
154
-
155
- if (effectiveTags.length) {
156
- candidates = candidates.filter((r) => {
157
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
158
- return effectiveTags.some((t) => entryTags.includes(t));
159
- });
160
- }
161
-
162
- const noiseIds = candidates
163
- .filter((r) => NOISE_KINDS.has(r.kind))
164
- .map((r) => r.id);
165
-
166
- const synthesisEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
167
-
168
- if (synthesisEntries.length === 0) {
169
- return err(
170
- `No entries found for topic "${topic}" to synthesize. Try a broader topic or different tags.`,
171
- "NO_ENTRIES",
172
- );
173
- }
174
-
175
- let briefBody;
176
- try {
177
- const prompt = buildSynthesisPrompt(topic, synthesisEntries);
178
- briefBody = await callLlm(prompt);
179
- } catch (e) {
180
- return err(
181
- `LLM synthesis failed: ${e.message}. Ensure ANTHROPIC_API_KEY is set.`,
182
- "LLM_ERROR",
183
- );
184
- }
185
-
186
- const effectiveIdentityKey =
187
- identity_key ?? `snapshot-${slugifyTopic(topic)}`;
188
-
189
- const briefTags = [
190
- "snapshot",
191
- ...(tags ?? []),
192
- ...(normalizedKinds.length > 0 ? [] : []),
193
- ];
194
-
195
- const supersedes = noiseIds.length > 0 ? noiseIds : undefined;
196
-
197
- const entry = await captureAndIndex(ctx, {
198
- kind: "brief",
199
- title: `${topic} — Context Brief`,
200
- body: briefBody,
201
- tags: briefTags,
202
- source: "create_snapshot",
203
- identity_key: effectiveIdentityKey,
204
- supersedes,
205
- userId,
206
- meta: {
207
- topic,
208
- entry_count: synthesisEntries.length,
209
- noise_superseded: noiseIds.length,
210
- synthesized_from: synthesisEntries.map((e) => e.id),
211
- },
212
- });
213
-
214
- const parts = [
215
- `✓ Snapshot created → id: ${entry.id}`,
216
- ` title: ${entry.title}`,
217
- ` identity_key: ${effectiveIdentityKey}`,
218
- ` synthesized from: ${synthesisEntries.length} entries`,
219
- noiseIds.length > 0
220
- ? ` noise superseded: ${noiseIds.length} entries`
221
- : null,
222
- "",
223
- "_Retrieve with: get_context(kind: 'brief', identity_key: '" +
224
- effectiveIdentityKey +
225
- "')_",
226
- ]
227
- .filter((l) => l !== null)
228
- .join("\n");
229
-
230
- return ok(parts);
231
- }
@@ -1,60 +0,0 @@
1
- import { z } from "zod";
2
- import { unlinkSync } from "node:fs";
3
- import { ok, err } from "../helpers.js";
4
-
5
- export const name = "delete_context";
6
-
7
- export const description =
8
- "Delete an entry from your vault by its ULID id. Removes the file from disk and cleans up the search index.";
9
-
10
- export const inputSchema = {
11
- id: z.string().describe("The entry ULID to delete"),
12
- };
13
-
14
- /**
15
- * @param {object} args
16
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
17
- * @param {import('../types.js').ToolShared} shared
18
- */
19
- export async function handler({ id }, ctx, { ensureIndexed }) {
20
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
21
-
22
- if (!id?.trim())
23
- return err("Required: id (non-empty string)", "INVALID_INPUT");
24
- await ensureIndexed();
25
-
26
- const entry = ctx.stmts.getEntryById.get(id);
27
- if (!entry) return err(`Entry not found: ${id}`, "NOT_FOUND");
28
-
29
- // Ownership check: don't leak existence across users
30
- if (userId !== undefined && entry.user_id !== userId) {
31
- return err(`Entry not found: ${id}`, "NOT_FOUND");
32
- }
33
-
34
- // Delete file from disk first (source of truth)
35
- let fileWarning = null;
36
- if (entry.file_path) {
37
- try {
38
- unlinkSync(entry.file_path);
39
- } catch (e) {
40
- // ENOENT = already gone — not an error worth surfacing
41
- if (e.code !== "ENOENT") {
42
- fileWarning = `file could not be removed from disk (${e.code}): ${entry.file_path}`;
43
- }
44
- }
45
- }
46
-
47
- // Delete vector embedding
48
- const rowidResult = ctx.stmts.getRowid.get(id);
49
- if (rowidResult?.rowid) {
50
- try {
51
- ctx.deleteVec(Number(rowidResult.rowid));
52
- } catch {}
53
- }
54
-
55
- // Delete DB row (FTS trigger handles FTS cleanup)
56
- ctx.stmts.deleteEntry.run(id);
57
-
58
- const msg = `Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`;
59
- return ok(fileWarning ? `${msg}\nWarning: ${fileWarning}` : msg);
60
- }