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,14 +1,14 @@
1
- import { z } from "zod";
2
- import { createHash } from "node:crypto";
3
- import { readFileSync, existsSync } from "node:fs";
4
- import { resolve } from "node:path";
5
- import { hybridSearch } from "@context-vault/core/search";
6
- import { categoryFor } from "@context-vault/core/categories";
7
- import { normalizeKind } from "@context-vault/core/files";
8
- import { resolveTemporalParams } from "../temporal.js";
9
- import { collectLinkedEntries } from "../linking.js";
10
- import { ok, err, errWithHint } from "../helpers.js";
11
- import { isEmbedAvailable } from "@context-vault/core/embed";
1
+ import { z } from 'zod';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { hybridSearch } from '@context-vault/core/search';
6
+ import { categoryFor } from '@context-vault/core/categories';
7
+ import { normalizeKind } from '@context-vault/core/files';
8
+ import { resolveTemporalParams } from '../temporal.js';
9
+ import { collectLinkedEntries } from '../linking.js';
10
+ import { ok, err, errWithHint } from '../helpers.js';
11
+ import { isEmbedAvailable } from '@context-vault/core/embed';
12
12
 
13
13
  const STALE_DUPLICATE_DAYS = 7;
14
14
  const DEFAULT_PIVOT_COUNT = 2;
@@ -22,21 +22,18 @@ const BRIEF_SCORE_BOOST = 0.05;
22
22
  * word boundary. Returns the truncated string with "..." appended.
23
23
  */
24
24
  export function skeletonBody(body) {
25
- if (!body) return "";
25
+ if (!body) return '';
26
26
  if (body.length <= SKELETON_BODY_CHARS) return body;
27
27
  const slice = body.slice(0, SKELETON_BODY_CHARS);
28
- const sentenceEnd = Math.max(
29
- slice.lastIndexOf(". "),
30
- slice.lastIndexOf(".\n"),
31
- );
28
+ const sentenceEnd = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('.\n'));
32
29
  if (sentenceEnd > SKELETON_BODY_CHARS * 0.4) {
33
- return slice.slice(0, sentenceEnd + 1) + "...";
30
+ return slice.slice(0, sentenceEnd + 1) + '...';
34
31
  }
35
- const wordEnd = slice.lastIndexOf(" ");
32
+ const wordEnd = slice.lastIndexOf(' ');
36
33
  if (wordEnd > SKELETON_BODY_CHARS * 0.4) {
37
- return slice.slice(0, wordEnd) + "...";
34
+ return slice.slice(0, wordEnd) + '...';
38
35
  }
39
- return slice + "...";
36
+ return slice + '...';
40
37
  }
41
38
 
42
39
  /**
@@ -65,15 +62,13 @@ export function detectConflicts(entries, _ctx) {
65
62
  conflicts.push({
66
63
  entry_a_id: entry.id,
67
64
  entry_b_id: entry.superseded_by,
68
- reason: "superseded",
65
+ reason: 'superseded',
69
66
  recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
70
67
  });
71
68
  }
72
69
  }
73
70
 
74
- const supersededConflictPairs = new Set(
75
- conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
76
- );
71
+ const supersededConflictPairs = new Set(conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`));
77
72
 
78
73
  for (let i = 0; i < entries.length; i++) {
79
74
  for (let j = i + 1; j < entries.length; j++) {
@@ -109,7 +104,7 @@ export function detectConflicts(entries, _ctx) {
109
104
  conflicts.push({
110
105
  entry_a_id: older.id,
111
106
  entry_b_id: newer.id,
112
- reason: "stale_duplicate",
107
+ reason: 'stale_duplicate',
113
108
  recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
114
109
  });
115
110
  }
@@ -140,7 +135,7 @@ export function detectConsolidationHints(entries, db, opts = {}) {
140
135
 
141
136
  const candidateTags = new Set();
142
137
  for (const entry of entries) {
143
- if (entry.kind === "brief") continue;
138
+ if (entry.kind === 'brief') continue;
144
139
  const entryTags = entry.tags ? JSON.parse(entry.tags) : [];
145
140
  for (const tag of entryTags) candidateTags.add(tag);
146
141
  }
@@ -155,11 +150,11 @@ export function detectConsolidationHints(entries, db, opts = {}) {
155
150
  try {
156
151
  // When userId is defined (hosted mode), scope to that user.
157
152
  // When userId is undefined (local mode), no user scoping — column may not exist.
158
- const userClause = "";
153
+ const userClause = '';
159
154
  const countParams = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
160
155
  const countRow = db
161
156
  .prepare(
162
- `SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ?${userClause} AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`,
157
+ `SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ?${userClause} AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`
163
158
  )
164
159
  .get(...countParams);
165
160
  vaultCount = countRow?.c ?? 0;
@@ -171,17 +166,17 @@ export function detectConsolidationHints(entries, db, opts = {}) {
171
166
 
172
167
  let lastSnapshotAgeDays = null;
173
168
  try {
174
- const userClause = "";
169
+ const userClause = '';
175
170
  const params = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
176
171
  const recentBrief = db
177
172
  .prepare(
178
- `SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1`,
173
+ `SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1`
179
174
  )
180
175
  .get(...params);
181
176
 
182
177
  if (recentBrief) {
183
178
  lastSnapshotAgeDays = Math.round(
184
- (Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000,
179
+ (Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
185
180
  );
186
181
  if (recentBrief.created_at >= cutoff) continue;
187
182
  }
@@ -219,18 +214,16 @@ function checkStaleness(entry) {
219
214
 
220
215
  for (const sf of sourceFiles) {
221
216
  try {
222
- const absPath = sf.path.startsWith("/")
223
- ? sf.path
224
- : resolve(process.cwd(), sf.path);
217
+ const absPath = sf.path.startsWith('/') ? sf.path : resolve(process.cwd(), sf.path);
225
218
  if (!existsSync(absPath)) {
226
- return { stale: true, stale_reason: "source file not found" };
219
+ return { stale: true, stale_reason: 'source file not found' };
227
220
  }
228
221
  const contents = readFileSync(absPath);
229
- const currentHash = createHash("sha256").update(contents).digest("hex");
222
+ const currentHash = createHash('sha256').update(contents).digest('hex');
230
223
  if (currentHash !== sf.hash) {
231
224
  return {
232
225
  stale: true,
233
- stale_reason: "source file modified since observation",
226
+ stale_reason: 'source file modified since observation',
234
227
  };
235
228
  }
236
229
  } catch {
@@ -240,107 +233,95 @@ function checkStaleness(entry) {
240
233
  return null;
241
234
  }
242
235
 
243
- export const name = "get_context";
236
+ export const name = 'get_context';
244
237
 
245
238
  export const description =
246
- "Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context. Each result includes an `id` you can use with save_context or delete_context.";
239
+ 'Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context. Each result includes an `id` you can use with save_context or delete_context.';
247
240
 
248
241
  export const inputSchema = {
249
242
  query: z
250
243
  .string()
251
244
  .optional()
252
245
  .describe(
253
- "Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.",
246
+ 'Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.'
254
247
  ),
255
- kind: z
256
- .string()
257
- .optional()
258
- .describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
259
- category: z
260
- .enum(["knowledge", "entity", "event"])
261
- .optional()
262
- .describe("Filter by category"),
248
+ kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
249
+ category: z.enum(['knowledge', 'entity', 'event']).optional().describe('Filter by category'),
263
250
  identity_key: z
264
251
  .string()
265
252
  .optional()
266
- .describe("For entity lookup: exact match on identity key. Requires kind."),
253
+ .describe('For entity lookup: exact match on identity key. Requires kind.'),
267
254
  tags: z
268
255
  .array(z.string())
269
256
  .optional()
270
257
  .describe(
271
- "Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub']).",
258
+ "Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
272
259
  ),
273
260
  buckets: z
274
261
  .array(z.string())
275
262
  .optional()
276
263
  .describe(
277
- "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).",
264
+ "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)."
278
265
  ),
279
266
  since: z
280
267
  .string()
281
268
  .optional()
282
269
  .describe(
283
- "Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable.",
270
+ "Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable."
284
271
  ),
285
272
  until: z
286
273
  .string()
287
274
  .optional()
288
275
  .describe(
289
- "Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday.",
276
+ "Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday."
290
277
  ),
291
- limit: z
292
- .number()
293
- .max(500)
294
- .optional()
295
- .describe("Max results to return (default 10)"),
278
+ limit: z.number().max(500).optional().describe('Max results to return (default 10)'),
296
279
  include_superseded: z
297
280
  .boolean()
298
281
  .optional()
299
- .describe(
300
- "If true, include entries that have been superseded by newer ones. Default: false.",
301
- ),
282
+ .describe('If true, include entries that have been superseded by newer ones. Default: false.'),
302
283
  detect_conflicts: z
303
284
  .boolean()
304
285
  .optional()
305
286
  .describe(
306
- "If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.",
287
+ 'If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.'
307
288
  ),
308
289
  max_tokens: z
309
290
  .number()
310
291
  .max(100000)
311
292
  .optional()
312
293
  .describe(
313
- "Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.",
294
+ 'Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.'
314
295
  ),
315
296
  pivot_count: z
316
297
  .number()
317
298
  .optional()
318
299
  .describe(
319
- "Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.",
300
+ 'Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.'
320
301
  ),
321
302
  include_ephemeral: z
322
303
  .boolean()
323
304
  .optional()
324
305
  .describe(
325
- "If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.",
306
+ 'If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.'
326
307
  ),
327
308
  include_events: z
328
309
  .boolean()
329
310
  .optional()
330
311
  .describe(
331
- "If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.",
312
+ 'If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.'
332
313
  ),
333
314
  scope: z
334
- .enum(["hot", "events", "all"])
315
+ .enum(['hot', 'events', 'all'])
335
316
  .optional()
336
317
  .describe(
337
- "Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set.",
318
+ "Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set."
338
319
  ),
339
320
  follow_links: z
340
321
  .boolean()
341
322
  .optional()
342
323
  .describe(
343
- "If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.",
324
+ 'If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.'
344
325
  ),
345
326
  };
346
327
 
@@ -370,7 +351,7 @@ export async function handler(
370
351
  follow_links,
371
352
  },
372
353
  ctx,
373
- { ensureIndexed, reindexFailed },
354
+ { ensureIndexed, reindexFailed }
374
355
  ) {
375
356
  const { config } = ctx;
376
357
 
@@ -387,28 +368,21 @@ export async function handler(
387
368
  // scope "all": no category restriction — full vault
388
369
  let effectiveScope = scope;
389
370
  if (!effectiveScope) {
390
- effectiveScope = include_events ? "all" : "hot";
371
+ effectiveScope = include_events ? 'all' : 'hot';
391
372
  }
392
373
 
393
374
  // Scope "events" forces category to "event" unless caller already set a narrower category
394
- const scopedCategory =
395
- !category && effectiveScope === "events" ? "event" : category;
396
- const shouldExcludeEvents =
397
- hasQuery && effectiveScope === "hot" && !scopedCategory;
375
+ const scopedCategory = !category && effectiveScope === 'events' ? 'event' : category;
376
+ const shouldExcludeEvents = hasQuery && effectiveScope === 'hot' && !scopedCategory;
398
377
  // Expand buckets to bucket: prefixed tags and merge with explicit tags
399
378
  const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
400
379
  const effectiveTags = [...(tags ?? []), ...bucketTags];
401
380
  const hasFilters =
402
- kind ||
403
- scopedCategory ||
404
- effectiveTags.length ||
405
- since ||
406
- until ||
407
- identity_key;
381
+ kind || scopedCategory || effectiveTags.length || since || until || identity_key;
408
382
  if (!hasQuery && !hasFilters)
409
383
  return err(
410
- "Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
411
- "INVALID_INPUT",
384
+ 'Required: query or at least one filter (kind, category, tags, since, until, identity_key)',
385
+ 'INVALID_INPUT'
412
386
  );
413
387
  await ensureIndexed();
414
388
 
@@ -416,34 +390,32 @@ export async function handler(
416
390
 
417
391
  // Gap 1: Entity exact-match by identity_key
418
392
  if (identity_key) {
419
- if (!kindFilter)
420
- return err("identity_key requires kind to be specified", "INVALID_INPUT");
393
+ if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
421
394
  const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key);
422
395
  if (match) {
423
396
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
424
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
397
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
425
398
  const relPath =
426
399
  match.file_path && config.vaultDir
427
- ? match.file_path.replace(config.vaultDir + "/", "")
428
- : match.file_path || "n/a";
400
+ ? match.file_path.replace(config.vaultDir + '/', '')
401
+ : match.file_path || 'n/a';
429
402
  const lines = [
430
403
  `## Entity Match (exact)\n`,
431
- `### ${match.title || "(untitled)"} [${match.kind}/${match.category}]`,
404
+ `### ${match.title || '(untitled)'} [${match.kind}/${match.category}]`,
432
405
  `1.000 · ${tagStr} · ${relPath} · id: \`${match.id}\``,
433
- match.body?.slice(0, 300) + (match.body?.length > 300 ? "..." : ""),
406
+ match.body?.slice(0, 300) + (match.body?.length > 300 ? '...' : ''),
434
407
  ];
435
- return ok(lines.join("\n"));
408
+ return ok(lines.join('\n'));
436
409
  }
437
410
  // Fall through to semantic search as fallback
438
411
  }
439
412
 
440
413
  // Gap 2: Event default time-window
441
- const effectiveCategory =
442
- scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
414
+ const effectiveCategory = scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
443
415
  let effectiveSince = since || null;
444
416
  let effectiveUntil = until || null;
445
417
  let autoWindowed = false;
446
- if (effectiveCategory === "event" && !since && !until) {
418
+ if (effectiveCategory === 'event' && !since && !until) {
447
419
  const decayMs = (config.eventDecayDays || 30) * 86400000;
448
420
  effectiveSince = new Date(Date.now() - decayMs).toISOString();
449
421
  autoWindowed = true;
@@ -487,39 +459,37 @@ export async function handler(
487
459
  if (false) {
488
460
  }
489
461
  if (kindFilter) {
490
- clauses.push("kind = ?");
462
+ clauses.push('kind = ?');
491
463
  params.push(kindFilter);
492
464
  }
493
465
  if (scopedCategory) {
494
- clauses.push("category = ?");
466
+ clauses.push('category = ?');
495
467
  params.push(scopedCategory);
496
468
  }
497
469
  if (effectiveSince) {
498
- clauses.push("created_at >= ?");
470
+ clauses.push('created_at >= ?');
499
471
  params.push(effectiveSince);
500
472
  }
501
473
  if (effectiveUntil) {
502
- clauses.push("created_at <= ?");
474
+ clauses.push('created_at <= ?');
503
475
  params.push(effectiveUntil);
504
476
  }
505
477
  clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
506
478
  if (!include_superseded) {
507
- clauses.push("superseded_by IS NULL");
479
+ clauses.push('superseded_by IS NULL');
508
480
  }
509
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
481
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
510
482
  params.push(fetchLimit);
511
483
  let rows;
512
484
  try {
513
485
  rows = ctx.db
514
- .prepare(
515
- `SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`,
516
- )
486
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
517
487
  .all(...params);
518
488
  } catch (e) {
519
489
  return errWithHint(
520
490
  e.message,
521
- "DB_ERROR",
522
- "context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
491
+ 'DB_ERROR',
492
+ 'context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
523
493
  );
524
494
  }
525
495
 
@@ -540,18 +510,18 @@ export async function handler(
540
510
  // Brief score boost: briefs rank slightly higher so consolidated snapshots
541
511
  // surface above the individual entries they summarize.
542
512
  for (const r of filtered) {
543
- if (r.kind === "brief") r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
513
+ if (r.kind === 'brief') r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
544
514
  }
545
515
  filtered.sort((a, b) => b.score - a.score);
546
516
 
547
517
  // Tier filter: exclude ephemeral entries by default (NULL tier treated as working)
548
518
  if (!include_ephemeral) {
549
- filtered = filtered.filter((r) => r.tier !== "ephemeral");
519
+ filtered = filtered.filter((r) => r.tier !== 'ephemeral');
550
520
  }
551
521
 
552
522
  // Event category filter: exclude events from semantic search by default
553
523
  if (shouldExcludeEvents) {
554
- filtered = filtered.filter((r) => r.category !== "event");
524
+ filtered = filtered.filter((r) => r.category !== 'event');
555
525
  }
556
526
 
557
527
  if (!filtered.length) {
@@ -560,13 +530,11 @@ export async function handler(
560
530
  return ok(
561
531
  hasQuery
562
532
  ? `No results found for "${query}" in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
563
- : `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`,
533
+ : `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
564
534
  );
565
535
  }
566
536
  return ok(
567
- hasQuery
568
- ? "No results found for: " + query
569
- : "No entries found matching the given filters.",
537
+ hasQuery ? 'No results found for: ' + query : 'No entries found matching the given filters.'
570
538
  );
571
539
  }
572
540
 
@@ -602,8 +570,7 @@ export async function handler(
602
570
  }
603
571
 
604
572
  // Skeleton mode: determine pivot threshold
605
- const effectivePivot =
606
- pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
573
+ const effectivePivot = pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
607
574
 
608
575
  // Conflict detection
609
576
  const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
@@ -611,45 +578,43 @@ export async function handler(
611
578
  const lines = [];
612
579
  if (reindexFailed)
613
580
  lines.push(
614
- `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
581
+ `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`
615
582
  );
616
583
  if (hasQuery && isEmbedAvailable() === false)
617
584
  lines.push(
618
- `> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`,
585
+ `> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`
619
586
  );
620
- const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
587
+ const heading = hasQuery ? `Results for "${query}"` : 'Filtered entries';
621
588
  lines.push(`## ${heading} (${filtered.length} matches)\n`);
622
589
  if (tokensBudget != null) {
623
- lines.push(
624
- `> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`,
625
- );
590
+ lines.push(`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`);
626
591
  }
627
592
  if (autoWindowed) {
628
593
  const days = config.eventDecayDays || 30;
629
594
  lines.push(
630
- `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`,
595
+ `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
631
596
  );
632
597
  }
633
598
  for (let i = 0; i < filtered.length; i++) {
634
599
  const r = filtered[i];
635
600
  const isSkeleton = i >= effectivePivot;
636
601
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
637
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
602
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
638
603
  const relPath =
639
604
  r.file_path && config.vaultDir
640
- ? r.file_path.replace(config.vaultDir + "/", "")
641
- : r.file_path || "n/a";
642
- const skeletonLabel = isSkeleton ? " ⊘ skeleton" : "";
605
+ ? r.file_path.replace(config.vaultDir + '/', '')
606
+ : r.file_path || 'n/a';
607
+ const skeletonLabel = isSkeleton ? ' ⊘ skeleton' : '';
643
608
  lines.push(
644
- `### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]${skeletonLabel}`,
609
+ `### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
645
610
  );
646
611
  const dateStr =
647
612
  r.updated_at && r.updated_at !== r.created_at
648
613
  ? `${r.created_at} (updated ${r.updated_at})`
649
- : r.created_at || "";
650
- const tierStr = r.tier ? ` · tier: ${r.tier}` : "";
614
+ : r.created_at || '';
615
+ const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
651
616
  lines.push(
652
- `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``,
617
+ `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
653
618
  );
654
619
  const stalenessResult = checkStaleness(r);
655
620
  if (stalenessResult) {
@@ -660,25 +625,21 @@ export async function handler(
660
625
  if (isSkeleton) {
661
626
  lines.push(skeletonBody(r.body));
662
627
  } else {
663
- lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
628
+ lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? '...' : ''));
664
629
  }
665
- lines.push("");
630
+ lines.push('');
666
631
  }
667
632
 
668
633
  if (detect_conflicts) {
669
634
  if (conflicts.length === 0) {
670
- lines.push(
671
- `## Conflict Detection\n\nNo conflicts detected among results.\n`,
672
- );
635
+ lines.push(`## Conflict Detection\n\nNo conflicts detected among results.\n`);
673
636
  } else {
674
637
  lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
675
638
  for (const c of conflicts) {
676
- lines.push(
677
- `- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
678
- );
639
+ lines.push(`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``);
679
640
  lines.push(` Recommendation: ${c.recommendation}`);
680
641
  }
681
- lines.push("");
642
+ lines.push('');
682
643
  }
683
644
  }
684
645
 
@@ -696,21 +657,17 @@ export async function handler(
696
657
  if (uniqueLinked.length > 0) {
697
658
  lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
698
659
  for (const r of uniqueLinked) {
699
- const direction = forward.some((f) => f.id === r.id)
700
- ? "→ forward"
701
- : "← backlink";
660
+ const direction = forward.some((f) => f.id === r.id) ? '→ forward' : '← backlink';
702
661
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
703
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
662
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
704
663
  const relPath =
705
664
  r.file_path && config.vaultDir
706
- ? r.file_path.replace(config.vaultDir + "/", "")
707
- : r.file_path || "n/a";
708
- lines.push(
709
- `### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
710
- );
665
+ ? r.file_path.replace(config.vaultDir + '/', '')
666
+ : r.file_path || 'n/a';
667
+ lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
711
668
  lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
712
- lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? "..." : ""));
713
- lines.push("");
669
+ lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? '...' : ''));
670
+ lines.push('');
714
671
  }
715
672
  } else {
716
673
  lines.push(`## Linked Entries\n\nNo related entries found.\n`);
@@ -719,24 +676,19 @@ export async function handler(
719
676
 
720
677
  // Consolidation suggestion detection — lazy, opportunistic, vault-wide
721
678
  const consolidationOpts = {
722
- tagThreshold:
723
- config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
724
- maxAgeDays:
725
- config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
679
+ tagThreshold: config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
680
+ maxAgeDays: config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
726
681
  };
727
682
  const consolidationSuggestions = detectConsolidationHints(
728
683
  filtered,
729
684
  ctx.db,
730
685
 
731
- consolidationOpts,
686
+ consolidationOpts
732
687
  );
733
688
 
734
689
  // Auto-consolidate: fire-and-forget create_snapshot for eligible tags
735
- if (
736
- config.consolidation?.autoConsolidate &&
737
- consolidationSuggestions.length > 0
738
- ) {
739
- const { handler: snapshotHandler } = await import("./create-snapshot.js");
690
+ if (config.consolidation?.autoConsolidate && consolidationSuggestions.length > 0) {
691
+ const { handler: snapshotHandler } = await import('./create-snapshot.js');
740
692
  for (const suggestion of consolidationSuggestions) {
741
693
  snapshotHandler({ topic: suggestion.tag, tags: [suggestion.tag] }, ctx, {
742
694
  ensureIndexed: async () => {},
@@ -744,7 +696,7 @@ export async function handler(
744
696
  }
745
697
  }
746
698
 
747
- const result = ok(lines.join("\n"));
699
+ const result = ok(lines.join('\n'));
748
700
  const meta = {};
749
701
  meta.scope = effectiveScope;
750
702
  if (tokensBudget != null) {