context-vault 3.1.6 → 3.1.7

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 (82) hide show
  1. package/bin/cli.js +1369 -1774
  2. package/node_modules/@context-vault/core/dist/capture.d.ts +1 -1
  3. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  4. package/node_modules/@context-vault/core/dist/capture.js +34 -47
  5. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  6. package/node_modules/@context-vault/core/dist/categories.js +30 -30
  7. package/node_modules/@context-vault/core/dist/config.d.ts +1 -1
  8. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  9. package/node_modules/@context-vault/core/dist/config.js +37 -43
  10. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  11. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  12. package/node_modules/@context-vault/core/dist/constants.js +4 -4
  13. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  14. package/node_modules/@context-vault/core/dist/db.d.ts +2 -2
  15. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  16. package/node_modules/@context-vault/core/dist/db.js +21 -20
  17. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  18. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -1
  19. package/node_modules/@context-vault/core/dist/embed.js +11 -11
  20. package/node_modules/@context-vault/core/dist/embed.js.map +1 -1
  21. package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -1
  22. package/node_modules/@context-vault/core/dist/files.js +12 -13
  23. package/node_modules/@context-vault/core/dist/files.js.map +1 -1
  24. package/node_modules/@context-vault/core/dist/formatters.js +5 -5
  25. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  26. package/node_modules/@context-vault/core/dist/frontmatter.js +23 -23
  27. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  28. package/node_modules/@context-vault/core/dist/index.d.ts +1 -1
  29. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  30. package/node_modules/@context-vault/core/dist/index.js +58 -46
  31. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  32. package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -1
  33. package/node_modules/@context-vault/core/dist/ingest-url.js +30 -33
  34. package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -1
  35. package/node_modules/@context-vault/core/dist/main.d.ts +13 -13
  36. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  37. package/node_modules/@context-vault/core/dist/main.js +12 -12
  38. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  39. package/node_modules/@context-vault/core/dist/search.d.ts +1 -1
  40. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  41. package/node_modules/@context-vault/core/dist/search.js +20 -22
  42. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  43. package/node_modules/@context-vault/core/dist/types.d.ts +1 -1
  44. package/node_modules/@context-vault/core/package.json +1 -1
  45. package/node_modules/@context-vault/core/src/capture.ts +44 -81
  46. package/node_modules/@context-vault/core/src/categories.ts +30 -30
  47. package/node_modules/@context-vault/core/src/config.ts +45 -60
  48. package/node_modules/@context-vault/core/src/constants.ts +8 -10
  49. package/node_modules/@context-vault/core/src/db.ts +37 -56
  50. package/node_modules/@context-vault/core/src/embed.ts +15 -26
  51. package/node_modules/@context-vault/core/src/files.ts +13 -16
  52. package/node_modules/@context-vault/core/src/formatters.ts +5 -5
  53. package/node_modules/@context-vault/core/src/frontmatter.ts +26 -30
  54. package/node_modules/@context-vault/core/src/index.ts +94 -100
  55. package/node_modules/@context-vault/core/src/ingest-url.ts +56 -93
  56. package/node_modules/@context-vault/core/src/main.ts +13 -18
  57. package/node_modules/@context-vault/core/src/search.ts +34 -56
  58. package/node_modules/@context-vault/core/src/types.ts +1 -1
  59. package/package.json +2 -2
  60. package/scripts/postinstall.js +18 -25
  61. package/scripts/prepack.js +13 -19
  62. package/src/archive.js +211 -0
  63. package/src/error-log.js +7 -7
  64. package/src/helpers.js +11 -13
  65. package/src/linking.js +8 -11
  66. package/src/migrate-dirs.js +139 -0
  67. package/src/register-tools.js +46 -48
  68. package/src/server.js +73 -99
  69. package/src/status.js +35 -71
  70. package/src/telemetry.js +18 -22
  71. package/src/temporal.js +19 -30
  72. package/src/tools/clear-context.js +15 -18
  73. package/src/tools/context-status.js +37 -57
  74. package/src/tools/create-snapshot.js +45 -57
  75. package/src/tools/delete-context.js +11 -12
  76. package/src/tools/get-context.js +112 -160
  77. package/src/tools/ingest-project.js +66 -86
  78. package/src/tools/ingest-url.js +25 -41
  79. package/src/tools/list-buckets.js +19 -25
  80. package/src/tools/list-context.js +35 -58
  81. package/src/tools/save-context.js +126 -182
  82. package/src/tools/session-start.js +46 -62
@@ -1,17 +1,11 @@
1
- import { z } from "zod";
2
- import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
3
- import { indexEntry } from "@context-vault/core/index";
4
- import { categoryFor, defaultTierFor } from "@context-vault/core/categories";
5
- import { normalizeKind } from "@context-vault/core/files";
6
- import {
7
- ok,
8
- err,
9
- errWithHint,
10
- ensureVaultExists,
11
- ensureValidKind,
12
- } from "../helpers.js";
13
- import { maybeShowFeedbackPrompt } from "../telemetry.js";
14
- import { validateRelatedTo } from "../linking.js";
1
+ import { z } from 'zod';
2
+ import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
3
+ import { indexEntry } from '@context-vault/core/index';
4
+ import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
5
+ import { normalizeKind } from '@context-vault/core/files';
6
+ import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
7
+ import { maybeShowFeedbackPrompt } from '../telemetry.js';
8
+ import { validateRelatedTo } from '../linking.js';
15
9
  import {
16
10
  MAX_BODY_LENGTH,
17
11
  MAX_TITLE_LENGTH,
@@ -21,7 +15,7 @@ import {
21
15
  MAX_META_LENGTH,
22
16
  MAX_SOURCE_LENGTH,
23
17
  MAX_IDENTITY_KEY_LENGTH,
24
- } from "@context-vault/core/constants";
18
+ } from '@context-vault/core/constants';
25
19
 
26
20
  const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
27
21
  const SKIP_THRESHOLD = 0.95;
@@ -32,33 +26,31 @@ async function findSimilar(
32
26
  embedding,
33
27
  threshold,
34
28
 
35
- { hydrate = false } = {},
29
+ { hydrate = false } = {}
36
30
  ) {
37
31
  try {
38
- const vecCount = ctx.db
39
- .prepare("SELECT COUNT(*) as c FROM vault_vec")
40
- .get().c;
32
+ const vecCount = ctx.db.prepare('SELECT COUNT(*) as c FROM vault_vec').get().c;
41
33
  if (vecCount === 0) return [];
42
34
 
43
35
  const vecRows = ctx.db
44
36
  .prepare(
45
- `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
37
+ `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`
46
38
  )
47
39
  .all(embedding, 10);
48
40
 
49
41
  if (!vecRows.length) return [];
50
42
 
51
43
  const rowids = vecRows.map((vr) => vr.rowid);
52
- const placeholders = rowids.map(() => "?").join(",");
44
+ const placeholders = rowids.map(() => '?').join(',');
53
45
  // Local mode has no user_id column — omit it from the SELECT list.
54
- const isLocal = ctx.stmts._mode === "local";
46
+ const isLocal = ctx.stmts._mode === 'local';
55
47
  const columns = isLocal
56
48
  ? hydrate
57
- ? "rowid, id, title, body, kind, tags, category, updated_at"
58
- : "rowid, id, title, category"
49
+ ? 'rowid, id, title, body, kind, tags, category, updated_at'
50
+ : 'rowid, id, title, category'
59
51
  : hydrate
60
- ? "rowid, id, title, body, kind, tags, category, updated_at"
61
- : "rowid, id, title, category";
52
+ ? 'rowid, id, title, body, kind, tags, category, updated_at'
53
+ : 'rowid, id, title, category';
62
54
  const hydratedRows = ctx.db
63
55
  .prepare(`SELECT ${columns} FROM vault WHERE rowid IN (${placeholders})`)
64
56
  .all(...rowids);
@@ -72,7 +64,7 @@ async function findSimilar(
72
64
  if (similarity < threshold) continue;
73
65
  const row = byRowid.get(vr.rowid);
74
66
  if (!row) continue;
75
- if (row.category === "entity") continue;
67
+ if (row.category === 'entity') continue;
76
68
  const entry = { id: row.id, title: row.title, score: similarity };
77
69
  if (hydrate) {
78
70
  entry.body = row.body;
@@ -89,16 +81,14 @@ async function findSimilar(
89
81
  }
90
82
 
91
83
  function formatSimilarWarning(similar) {
92
- const lines = ["", "⚠ Similar entries already exist:"];
84
+ const lines = ['', '⚠ Similar entries already exist:'];
93
85
  for (const e of similar) {
94
86
  const score = e.score.toFixed(2);
95
- const title = e.title ? `"${e.title}"` : "(no title)";
87
+ const title = e.title ? `"${e.title}"` : '(no title)';
96
88
  lines.push(` - ${title} (${score}) — id: ${e.id}`);
97
89
  }
98
- lines.push(
99
- " Consider using `id: <existing>` in save_context to update instead.",
100
- );
101
- return lines.join("\n");
90
+ lines.push(' Consider using `id: <existing>` in save_context to update instead.');
91
+ return lines.join('\n');
102
92
  }
103
93
 
104
94
  export function buildConflictCandidates(similarEntries) {
@@ -107,31 +97,30 @@ export function buildConflictCandidates(similarEntries) {
107
97
  let reasoning_context;
108
98
 
109
99
  if (entry.score >= SKIP_THRESHOLD) {
110
- suggested_action = "SKIP";
100
+ suggested_action = 'SKIP';
111
101
  reasoning_context =
112
102
  `Near-duplicate detected (${(entry.score * 100).toFixed(0)}% similarity)` +
113
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
103
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
114
104
  `Content is nearly identical — saving would create a redundant entry. ` +
115
105
  `Use save_context with id: "${entry.id}" to update instead, or skip saving entirely.`;
116
106
  } else if (entry.score >= UPDATE_THRESHOLD) {
117
- suggested_action = "UPDATE";
107
+ suggested_action = 'UPDATE';
118
108
  reasoning_context =
119
109
  `High content similarity (${(entry.score * 100).toFixed(0)}%)` +
120
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
110
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
121
111
  `Likely the same knowledge — consider updating this entry via save_context with id: "${entry.id}".`;
122
112
  } else {
123
- suggested_action = "ADD";
113
+ suggested_action = 'ADD';
124
114
  reasoning_context =
125
115
  `Moderate similarity (${(entry.score * 100).toFixed(0)}%)` +
126
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
116
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
127
117
  `Content is related but distinct enough to coexist.`;
128
118
  }
129
119
 
130
120
  let parsedTags = [];
131
121
  if (entry.tags) {
132
122
  try {
133
- parsedTags =
134
- typeof entry.tags === "string" ? JSON.parse(entry.tags) : entry.tags;
123
+ parsedTags = typeof entry.tags === 'string' ? JSON.parse(entry.tags) : entry.tags;
135
124
  } catch {
136
125
  parsedTags = [];
137
126
  }
@@ -152,184 +141,145 @@ export function buildConflictCandidates(similarEntries) {
152
141
  }
153
142
 
154
143
  function formatConflictSuggestions(candidates) {
155
- const lines = ["", "── Conflict Resolution Suggestions ──"];
144
+ const lines = ['', '── Conflict Resolution Suggestions ──'];
156
145
  for (const c of candidates) {
157
- const titleDisplay = c.title ? `"${c.title}"` : "(no title)";
146
+ const titleDisplay = c.title ? `"${c.title}"` : '(no title)';
158
147
  lines.push(
159
- ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`,
148
+ ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`
160
149
  );
161
150
  lines.push(` ${c.reasoning_context}`);
162
151
  }
163
- return lines.join("\n");
152
+ return lines.join('\n');
164
153
  }
165
154
 
166
155
  /**
167
156
  * Validate input fields for save_context. Returns an error response or null.
168
157
  */
169
- function validateSaveInput({
170
- kind,
171
- title,
172
- body,
173
- tags,
174
- meta,
175
- source,
176
- identity_key,
177
- expires_at,
178
- }) {
158
+ function validateSaveInput({ kind, title, body, tags, meta, source, identity_key, expires_at }) {
179
159
  if (kind !== undefined && kind !== null) {
180
- if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
181
- return err(
182
- `kind must be a string, max ${MAX_KIND_LENGTH} chars`,
183
- "INVALID_INPUT",
184
- );
160
+ if (typeof kind !== 'string' || kind.length > MAX_KIND_LENGTH) {
161
+ return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, 'INVALID_INPUT');
185
162
  }
186
163
  }
187
164
  if (body !== undefined && body !== null) {
188
- if (typeof body !== "string" || body.length > MAX_BODY_LENGTH) {
189
- return err(
190
- `body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`,
191
- "INVALID_INPUT",
192
- );
165
+ if (typeof body !== 'string' || body.length > MAX_BODY_LENGTH) {
166
+ return err(`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`, 'INVALID_INPUT');
193
167
  }
194
168
  }
195
169
  if (title !== undefined && title !== null) {
196
- if (typeof title !== "string" || title.length > MAX_TITLE_LENGTH) {
197
- return err(
198
- `title must be a string, max ${MAX_TITLE_LENGTH} chars`,
199
- "INVALID_INPUT",
200
- );
170
+ if (typeof title !== 'string' || title.length > MAX_TITLE_LENGTH) {
171
+ return err(`title must be a string, max ${MAX_TITLE_LENGTH} chars`, 'INVALID_INPUT');
201
172
  }
202
173
  }
203
174
  if (tags !== undefined && tags !== null) {
204
- if (!Array.isArray(tags))
205
- return err("tags must be an array of strings", "INVALID_INPUT");
175
+ if (!Array.isArray(tags)) return err('tags must be an array of strings', 'INVALID_INPUT');
206
176
  if (tags.length > MAX_TAGS_COUNT)
207
- return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
177
+ return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, 'INVALID_INPUT');
208
178
  for (const tag of tags) {
209
- if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
210
- return err(
211
- `each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
212
- "INVALID_INPUT",
213
- );
179
+ if (typeof tag !== 'string' || tag.length > MAX_TAG_LENGTH) {
180
+ return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, 'INVALID_INPUT');
214
181
  }
215
182
  }
216
183
  }
217
184
  if (meta !== undefined && meta !== null) {
218
185
  const metaStr = JSON.stringify(meta);
219
186
  if (metaStr.length > MAX_META_LENGTH) {
220
- return err(
221
- `meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`,
222
- "INVALID_INPUT",
223
- );
187
+ return err(`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`, 'INVALID_INPUT');
224
188
  }
225
189
  }
226
190
  if (source !== undefined && source !== null) {
227
- if (typeof source !== "string" || source.length > MAX_SOURCE_LENGTH) {
228
- return err(
229
- `source must be a string, max ${MAX_SOURCE_LENGTH} chars`,
230
- "INVALID_INPUT",
231
- );
191
+ if (typeof source !== 'string' || source.length > MAX_SOURCE_LENGTH) {
192
+ return err(`source must be a string, max ${MAX_SOURCE_LENGTH} chars`, 'INVALID_INPUT');
232
193
  }
233
194
  }
234
195
  if (identity_key !== undefined && identity_key !== null) {
235
- if (
236
- typeof identity_key !== "string" ||
237
- identity_key.length > MAX_IDENTITY_KEY_LENGTH
238
- ) {
196
+ if (typeof identity_key !== 'string' || identity_key.length > MAX_IDENTITY_KEY_LENGTH) {
239
197
  return err(
240
198
  `identity_key must be a string, max ${MAX_IDENTITY_KEY_LENGTH} chars`,
241
- "INVALID_INPUT",
199
+ 'INVALID_INPUT'
242
200
  );
243
201
  }
244
202
  }
245
203
  if (expires_at !== undefined && expires_at !== null) {
246
- if (
247
- typeof expires_at !== "string" ||
248
- isNaN(new Date(expires_at).getTime())
249
- ) {
250
- return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
204
+ if (typeof expires_at !== 'string' || isNaN(new Date(expires_at).getTime())) {
205
+ return err('expires_at must be a valid ISO date string', 'INVALID_INPUT');
251
206
  }
252
207
  }
253
208
  return null;
254
209
  }
255
210
 
256
- export const name = "save_context";
211
+ export const name = 'save_context';
257
212
 
258
213
  export const description =
259
- "Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind. To update an existing entry, pass its `id` — omitted fields are preserved.";
214
+ 'Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind. To update an existing entry, pass its `id` — omitted fields are preserved.';
260
215
 
261
216
  export const inputSchema = {
262
217
  id: z
263
218
  .string()
264
219
  .optional()
265
220
  .describe(
266
- "Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.",
221
+ 'Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.'
267
222
  ),
268
223
  kind: z
269
224
  .string()
270
225
  .optional()
271
226
  .describe(
272
- "Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries.",
227
+ "Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
273
228
  ),
274
- title: z.string().optional().describe("Entry title (optional for insights)"),
275
- body: z
276
- .string()
277
- .optional()
278
- .describe("Main content. Required for new entries."),
229
+ title: z.string().optional().describe('Entry title (optional for insights)'),
230
+ body: z.string().optional().describe('Main content. Required for new entries.'),
279
231
  tags: z
280
232
  .array(z.string())
281
233
  .optional()
282
234
  .describe(
283
- "Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval.",
235
+ "Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
284
236
  ),
285
237
  meta: z
286
238
  .any()
287
239
  .optional()
288
240
  .describe(
289
- "Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })",
241
+ "Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
290
242
  ),
291
243
  folder: z
292
244
  .string()
293
245
  .optional()
294
246
  .describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
295
- source: z.string().optional().describe("Where this knowledge came from"),
247
+ source: z.string().optional().describe('Where this knowledge came from'),
296
248
  identity_key: z
297
249
  .string()
298
250
  .optional()
299
251
  .describe(
300
- "Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.",
252
+ 'Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.'
301
253
  ),
302
- expires_at: z.string().optional().describe("ISO date for TTL expiry"),
254
+ expires_at: z.string().optional().describe('ISO date for TTL expiry'),
303
255
  supersedes: z
304
256
  .array(z.string())
305
257
  .optional()
306
258
  .describe(
307
- "Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.",
259
+ 'Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.'
308
260
  ),
309
261
  related_to: z
310
262
  .array(z.string())
311
263
  .optional()
312
264
  .describe(
313
- "Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.",
265
+ 'Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.'
314
266
  ),
315
267
  source_files: z
316
268
  .array(
317
269
  z.object({
318
- path: z.string().describe("File path (absolute or relative to cwd)"),
319
- hash: z
320
- .string()
321
- .describe("SHA-256 hash of the file contents at observation time"),
322
- }),
270
+ path: z.string().describe('File path (absolute or relative to cwd)'),
271
+ hash: z.string().describe('SHA-256 hash of the file contents at observation time'),
272
+ })
323
273
  )
324
274
  .optional()
325
275
  .describe(
326
- "Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.",
276
+ 'Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.'
327
277
  ),
328
278
  dry_run: z
329
279
  .boolean()
330
280
  .optional()
331
281
  .describe(
332
- "If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.",
282
+ 'If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.'
333
283
  ),
334
284
  similarity_threshold: z
335
285
  .number()
@@ -337,19 +287,19 @@ export const inputSchema = {
337
287
  .max(1)
338
288
  .optional()
339
289
  .describe(
340
- "Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.",
290
+ 'Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.'
341
291
  ),
342
292
  tier: z
343
- .enum(["ephemeral", "working", "durable"])
293
+ .enum(['ephemeral', 'working', 'durable'])
344
294
  .optional()
345
295
  .describe(
346
- "Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified.",
296
+ "Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified."
347
297
  ),
348
298
  conflict_resolution: z
349
- .enum(["suggest", "off"])
299
+ .enum(['suggest', 'off'])
350
300
  .optional()
351
301
  .describe(
352
- 'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).',
302
+ 'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).'
353
303
  ),
354
304
  };
355
305
 
@@ -379,16 +329,16 @@ export async function handler(
379
329
  conflict_resolution,
380
330
  },
381
331
  ctx,
382
- { ensureIndexed },
332
+ { ensureIndexed }
383
333
  ) {
384
334
  const { config } = ctx;
385
- const suggestMode = conflict_resolution !== "off";
335
+ const suggestMode = conflict_resolution !== 'off';
386
336
 
387
337
  const vaultErr = ensureVaultExists(config);
388
338
  if (vaultErr) return vaultErr;
389
339
 
390
340
  const relatedToErr = validateRelatedTo(related_to);
391
- if (relatedToErr) return err(relatedToErr, "INVALID_INPUT");
341
+ if (relatedToErr) return err(relatedToErr, 'INVALID_INPUT');
392
342
 
393
343
  const inputErr = validateSaveInput({
394
344
  kind,
@@ -404,21 +354,21 @@ export async function handler(
404
354
 
405
355
  // ── Update mode ──
406
356
  if (id) {
407
- await ensureIndexed();
357
+ await ensureIndexed({ blocking: false });
408
358
 
409
359
  const existing = ctx.stmts.getEntryById.get(id);
410
- if (!existing) return err(`Entry not found: ${id}`, "NOT_FOUND");
360
+ if (!existing) return err(`Entry not found: ${id}`, 'NOT_FOUND');
411
361
 
412
362
  if (kind && normalizeKind(kind) !== existing.kind) {
413
363
  return err(
414
364
  `Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`,
415
- "INVALID_UPDATE",
365
+ 'INVALID_UPDATE'
416
366
  );
417
367
  }
418
368
  if (identity_key && identity_key !== existing.identity_key) {
419
369
  return err(
420
370
  `Cannot change identity_key (current: "${existing.identity_key}"). Delete and re-create instead.`,
421
- "INVALID_UPDATE",
371
+ 'INVALID_UPDATE'
422
372
  );
423
373
  }
424
374
 
@@ -447,8 +397,8 @@ export async function handler(
447
397
  } catch (e) {
448
398
  return errWithHint(
449
399
  e.message,
450
- "UPDATE_FAILED",
451
- "context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
400
+ 'UPDATE_FAILED',
401
+ 'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
452
402
  );
453
403
  }
454
404
  if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
@@ -457,43 +407,41 @@ export async function handler(
457
407
  ctx.stmts.updateRelatedTo.run(null, entry.id);
458
408
  }
459
409
  const relPath = entry.filePath
460
- ? entry.filePath.replace(config.vaultDir + "/", "")
410
+ ? entry.filePath.replace(config.vaultDir + '/', '')
461
411
  : entry.filePath;
462
412
  const parts = [`✓ Updated ${entry.kind} → ${relPath}`, ` id: ${entry.id}`];
463
413
  if (entry.title) parts.push(` title: ${entry.title}`);
464
414
  const entryTags = entry.tags || [];
465
- if (entryTags.length) parts.push(` tags: ${entryTags.join(", ")}`);
466
- parts.push("", "_Search with get_context to verify changes._");
467
- return ok(parts.join("\n"));
415
+ if (entryTags.length) parts.push(` tags: ${entryTags.join(', ')}`);
416
+ parts.push('', '_Search with get_context to verify changes._');
417
+ return ok(parts.join('\n'));
468
418
  }
469
419
 
470
420
  // ── Create mode ──
471
- if (!kind) return err("Required: kind (for new entries)", "INVALID_INPUT");
421
+ if (!kind) return err('Required: kind (for new entries)', 'INVALID_INPUT');
472
422
  const kindErr = ensureValidKind(kind);
473
423
  if (kindErr) return kindErr;
474
- if (!body?.trim())
475
- return err("Required: body (for new entries)", "INVALID_INPUT");
424
+ if (!body?.trim()) return err('Required: body (for new entries)', 'INVALID_INPUT');
476
425
 
477
426
  // Normalize kind to canonical singular form (e.g. "insights" → "insight")
478
427
  const normalizedKind = normalizeKind(kind);
479
428
 
480
- if (categoryFor(normalizedKind) === "entity" && !identity_key) {
481
- return err(
482
- `Entity kind "${normalizedKind}" requires identity_key`,
483
- "MISSING_IDENTITY_KEY",
484
- );
429
+ if (categoryFor(normalizedKind) === 'entity' && !identity_key) {
430
+ return err(`Entity kind "${normalizedKind}" requires identity_key`, 'MISSING_IDENTITY_KEY');
485
431
  }
486
432
 
487
- await ensureIndexed();
433
+ // Start reindex in background but don't wait — similarity check
434
+ // may miss unindexed entries, but the save won't time out
435
+ await ensureIndexed({ blocking: false });
488
436
 
489
437
  // ── Similarity check (knowledge + event only) ────────────────────────────
490
438
  const category = categoryFor(normalizedKind);
491
439
  let similarEntries = [];
492
440
  let queryEmbedding = null;
493
441
 
494
- if (category === "knowledge" || category === "event") {
442
+ if (category === 'knowledge' || category === 'event') {
495
443
  const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
496
- const embeddingText = [title, body].filter(Boolean).join(" ");
444
+ const embeddingText = [title, body].filter(Boolean).join(' ');
497
445
  try {
498
446
  queryEmbedding = await ctx.embed(embeddingText);
499
447
  } catch {
@@ -505,43 +453,43 @@ export async function handler(
505
453
  queryEmbedding,
506
454
  threshold,
507
455
 
508
- { hydrate: suggestMode },
456
+ { hydrate: suggestMode }
509
457
  );
510
458
  }
511
459
  }
512
460
 
513
461
  if (dry_run) {
514
- const parts = ["(dry run — nothing saved)"];
462
+ const parts = ['(dry run — nothing saved)'];
515
463
  if (similarEntries.length) {
516
464
  if (suggestMode) {
517
465
  const candidates = buildConflictCandidates(similarEntries);
518
- parts.push("", "⚠ Similar entries already exist:");
466
+ parts.push('', '⚠ Similar entries already exist:');
519
467
  for (const e of similarEntries) {
520
468
  const score = e.score.toFixed(2);
521
- const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
469
+ const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
522
470
  parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
523
471
  }
524
472
  parts.push(formatConflictSuggestions(candidates));
525
473
  parts.push(
526
- "",
527
- "Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
474
+ '',
475
+ 'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
528
476
  );
529
477
  } else {
530
- parts.push("", "⚠ Similar entries already exist:");
478
+ parts.push('', '⚠ Similar entries already exist:');
531
479
  for (const e of similarEntries) {
532
480
  const score = e.score.toFixed(2);
533
- const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
481
+ const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
534
482
  parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
535
483
  }
536
484
  parts.push(
537
- "",
538
- "Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
485
+ '',
486
+ 'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
539
487
  );
540
488
  }
541
489
  } else {
542
- parts.push("", "No similar entries found. Safe to save.");
490
+ parts.push('', 'No similar entries found. Safe to save.');
543
491
  }
544
- return ok(parts.join("\n"));
492
+ return ok(parts.join('\n'));
545
493
  }
546
494
 
547
495
  const mergedMeta = { ...(meta || {}) };
@@ -550,7 +498,7 @@ export async function handler(
550
498
 
551
499
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
552
500
 
553
- const embeddingToReuse = category === "knowledge" ? queryEmbedding : null;
501
+ const embeddingToReuse = category === 'knowledge' ? queryEmbedding : null;
554
502
 
555
503
  let entry;
556
504
  try {
@@ -572,13 +520,13 @@ export async function handler(
572
520
 
573
521
  tier: effectiveTier,
574
522
  },
575
- embeddingToReuse,
523
+ embeddingToReuse
576
524
  );
577
525
  } catch (e) {
578
526
  return errWithHint(
579
527
  e.message,
580
- "SAVE_FAILED",
581
- "context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
528
+ 'SAVE_FAILED',
529
+ 'context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
582
530
  );
583
531
  }
584
532
 
@@ -587,37 +535,33 @@ export async function handler(
587
535
  }
588
536
 
589
537
  const relPath = entry.filePath
590
- ? entry.filePath.replace(config.vaultDir + "/", "")
538
+ ? entry.filePath.replace(config.vaultDir + '/', '')
591
539
  : entry.filePath;
592
540
  const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
593
541
  if (title) parts.push(` title: ${title}`);
594
- if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
542
+ if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
595
543
  parts.push(` tier: ${effectiveTier}`);
596
- parts.push("", "_Use this id to update or delete later._");
597
- const hasBucketTag = (tags || []).some(
598
- (t) => typeof t === "string" && t.startsWith("bucket:"),
599
- );
544
+ parts.push('', '_Use this id to update or delete later._');
545
+ const hasBucketTag = (tags || []).some((t) => typeof t === 'string' && t.startsWith('bucket:'));
600
546
  if (tags && tags.length > 0 && !hasBucketTag) {
601
547
  parts.push(
602
- "",
603
- "_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._",
548
+ '',
549
+ '_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._'
604
550
  );
605
551
  }
606
- const bucketTags = (tags || []).filter(
607
- (t) => typeof t === "string" && t.startsWith("bucket:"),
608
- );
552
+ const bucketTags = (tags || []).filter((t) => typeof t === 'string' && t.startsWith('bucket:'));
609
553
  for (const bt of bucketTags) {
610
- const bucketUserClause = "";
554
+ const bucketUserClause = '';
611
555
  const bucketParams = false ? [bt] : [bt];
612
556
  const exists = ctx.db
613
557
  .prepare(
614
- `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
558
+ `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`
615
559
  )
616
560
  .get(...bucketParams);
617
561
  if (!exists) {
618
562
  parts.push(
619
563
  ``,
620
- `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`,
564
+ `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`
621
565
  );
622
566
  }
623
567
  }
@@ -634,15 +578,15 @@ export async function handler(
634
578
  const criticalLimit = config.thresholds?.totalEntries?.critical;
635
579
  if (criticalLimit != null) {
636
580
  try {
637
- const countRow = ctx.db.prepare("SELECT COUNT(*) as c FROM vault").get();
581
+ const countRow = ctx.db.prepare('SELECT COUNT(*) as c FROM vault').get();
638
582
  if (countRow.c >= criticalLimit) {
639
583
  parts.push(
640
584
  ``,
641
- `ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`,
585
+ `ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`
642
586
  );
643
587
  }
644
588
  } catch {}
645
589
  }
646
590
 
647
- return ok(parts.join("\n"));
591
+ return ok(parts.join('\n'));
648
592
  }