@context-vault/core 2.17.1 → 3.0.3

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 -16
  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/{core/files.js → files.ts} +15 -20
  65. package/src/{capture/formatters.js → formatters.ts} +13 -11
  66. package/src/{core/frontmatter.js → frontmatter.ts} +26 -33
  67. package/src/index.ts +353 -0
  68. package/src/ingest-url.ts +99 -0
  69. package/src/main.ts +111 -0
  70. package/src/{retrieve/index.js → search.ts} +62 -150
  71. package/src/types.ts +166 -0
  72. package/src/capture/file-ops.js +0 -99
  73. package/src/capture/import-pipeline.js +0 -46
  74. package/src/capture/importers.js +0 -387
  75. package/src/capture/index.js +0 -250
  76. package/src/capture/ingest-url.js +0 -252
  77. package/src/consolidation/index.js +0 -112
  78. package/src/core/categories.js +0 -73
  79. package/src/core/error-log.js +0 -54
  80. package/src/core/linking.js +0 -161
  81. package/src/core/migrate-dirs.js +0 -196
  82. package/src/core/status.js +0 -350
  83. package/src/core/telemetry.js +0 -90
  84. package/src/core/temporal.js +0 -146
  85. package/src/index/db.js +0 -586
  86. package/src/index/index.js +0 -583
  87. package/src/index.js +0 -71
  88. package/src/server/helpers.js +0 -44
  89. package/src/server/tools/clear-context.js +0 -47
  90. package/src/server/tools/context-status.js +0 -182
  91. package/src/server/tools/create-snapshot.js +0 -200
  92. package/src/server/tools/delete-context.js +0 -60
  93. package/src/server/tools/get-context.js +0 -765
  94. package/src/server/tools/ingest-project.js +0 -244
  95. package/src/server/tools/ingest-url.js +0 -88
  96. package/src/server/tools/list-buckets.js +0 -116
  97. package/src/server/tools/list-context.js +0 -163
  98. package/src/server/tools/save-context.js +0 -632
  99. package/src/server/tools/session-start.js +0 -285
  100. package/src/server/tools.js +0 -172
  101. package/src/sync/sync.js +0 -235
@@ -1,583 +0,0 @@
1
- /**
2
- * Index Layer — Public API
3
- *
4
- * Owns the database as a derived index. Handles both bulk sync (reindex)
5
- * and single-entry indexing (indexEntry) for write-through capture.
6
- *
7
- * Agent Constraint: Can import ../core. Owns db.js and embed.js.
8
- */
9
-
10
- import { readFileSync, readdirSync, existsSync, unlinkSync } from "node:fs";
11
- import { join, basename } from "node:path";
12
- import { dirToKind, walkDir, ulid } from "../core/files.js";
13
- import {
14
- categoryFor,
15
- defaultTierFor,
16
- CATEGORY_DIRS,
17
- } from "../core/categories.js";
18
- import {
19
- parseFrontmatter,
20
- parseEntryFromMarkdown,
21
- } from "../core/frontmatter.js";
22
- import { embedBatch } from "./embed.js";
23
-
24
- const EXCLUDED_DIRS = new Set(["projects", "_archive"]);
25
- const EXCLUDED_FILES = new Set(["context.md", "memory.md", "README.md"]);
26
-
27
- const EMBED_BATCH_SIZE = 32;
28
-
29
- /**
30
- * Index a single entry with idempotent upsert behavior.
31
- * Called immediately after Capture Layer writes the file.
32
- *
33
- * For entities with identity_key: uses upsertByIdentityKey if existing row found.
34
- *
35
- * @param {import('../server/types.js').BaseCtx & Partial<import('../server/types.js').HostedCtxExtensions>} ctx
36
- * @param {{ id, kind, category, title, body, meta, tags, source, filePath, createdAt, identity_key, expires_at, userId }} entry
37
- */
38
- export async function indexEntry(
39
- ctx,
40
- {
41
- id,
42
- kind,
43
- category,
44
- title,
45
- body,
46
- meta,
47
- tags,
48
- source,
49
- filePath,
50
- createdAt,
51
- identity_key,
52
- expires_at,
53
- source_files,
54
- tier,
55
- userId,
56
- },
57
- ) {
58
- // Don't index entries that have already expired
59
- if (expires_at && new Date(expires_at) <= new Date()) {
60
- return;
61
- }
62
-
63
- const tagsJson = tags ? JSON.stringify(tags) : null;
64
- const metaJson = meta ? JSON.stringify(meta) : null;
65
- const sourceFilesJson = source_files ? JSON.stringify(source_files) : null;
66
- const cat = category || categoryFor(kind);
67
- const effectiveTier = tier || defaultTierFor(kind);
68
- const userIdVal = userId || null;
69
- const isLocal = ctx.stmts._mode === "local";
70
-
71
- let wasUpdate = false;
72
-
73
- // Entity upsert: check by (kind, identity_key[, user_id]) first.
74
- // Local mode omits user_id — all entries are user-agnostic.
75
- if (cat === "entity" && identity_key) {
76
- const existing = isLocal
77
- ? ctx.stmts.getByIdentityKey.get(kind, identity_key)
78
- : ctx.stmts.getByIdentityKey.get(kind, identity_key, userIdVal);
79
- if (existing) {
80
- if (isLocal) {
81
- ctx.stmts.upsertByIdentityKey.run(
82
- title || null,
83
- body,
84
- metaJson,
85
- tagsJson,
86
- source || "claude-code",
87
- cat,
88
- filePath,
89
- expires_at || null,
90
- sourceFilesJson,
91
- kind,
92
- identity_key,
93
- );
94
- } else {
95
- ctx.stmts.upsertByIdentityKey.run(
96
- title || null,
97
- body,
98
- metaJson,
99
- tagsJson,
100
- source || "claude-code",
101
- cat,
102
- filePath,
103
- expires_at || null,
104
- sourceFilesJson,
105
- kind,
106
- identity_key,
107
- userIdVal,
108
- );
109
- }
110
- wasUpdate = true;
111
- }
112
- }
113
-
114
- if (!wasUpdate) {
115
- // Prepare encryption if ctx.encrypt is available (hosted mode only)
116
- let encrypted = null;
117
- if (ctx.encrypt) {
118
- encrypted = await ctx.encrypt({ title, body, meta });
119
- }
120
-
121
- try {
122
- if (encrypted) {
123
- // Hosted-mode encrypted insert: store preview in body for FTS,
124
- // full content in encrypted columns.
125
- const bodyPreview = body.slice(0, 200);
126
- ctx.stmts.insertEntryEncrypted.run(
127
- id,
128
- userIdVal,
129
- kind,
130
- cat,
131
- title || null,
132
- bodyPreview,
133
- metaJson,
134
- tagsJson,
135
- source || "claude-code",
136
- filePath,
137
- identity_key || null,
138
- expires_at || null,
139
- createdAt,
140
- createdAt,
141
- encrypted.body_encrypted,
142
- encrypted.title_encrypted,
143
- encrypted.meta_encrypted,
144
- encrypted.iv,
145
- sourceFilesJson,
146
- effectiveTier,
147
- );
148
- } else if (isLocal) {
149
- // Local mode: no user_id column — 15 params.
150
- ctx.stmts.insertEntry.run(
151
- id,
152
- kind,
153
- cat,
154
- title || null,
155
- body,
156
- metaJson,
157
- tagsJson,
158
- source || "claude-code",
159
- filePath,
160
- identity_key || null,
161
- expires_at || null,
162
- createdAt,
163
- createdAt,
164
- sourceFilesJson,
165
- effectiveTier,
166
- );
167
- } else {
168
- // Hosted mode without encryption: 16 params (includes user_id).
169
- ctx.stmts.insertEntry.run(
170
- id,
171
- userIdVal,
172
- kind,
173
- cat,
174
- title || null,
175
- body,
176
- metaJson,
177
- tagsJson,
178
- source || "claude-code",
179
- filePath,
180
- identity_key || null,
181
- expires_at || null,
182
- createdAt,
183
- createdAt,
184
- sourceFilesJson,
185
- effectiveTier,
186
- );
187
- }
188
- } catch (e) {
189
- if (e.message.includes("UNIQUE constraint")) {
190
- ctx.stmts.updateEntry.run(
191
- title || null,
192
- body,
193
- metaJson,
194
- tagsJson,
195
- source || "claude-code",
196
- cat,
197
- identity_key || null,
198
- expires_at || null,
199
- filePath,
200
- );
201
- if (sourceFilesJson !== null && ctx.stmts.updateSourceFiles) {
202
- const entryRow = ctx.stmts.getRowidByPath.get(filePath);
203
- if (entryRow) {
204
- const idRow = ctx.db
205
- .prepare("SELECT id FROM vault WHERE file_path = ?")
206
- .get(filePath);
207
- if (idRow)
208
- ctx.stmts.updateSourceFiles.run(sourceFilesJson, idRow.id);
209
- }
210
- }
211
- wasUpdate = true;
212
- } else {
213
- throw e;
214
- }
215
- }
216
- }
217
-
218
- // After update, get rowid by file_path (since id might differ); otherwise by id
219
- const rowidResult = wasUpdate
220
- ? ctx.stmts.getRowidByPath.get(filePath)
221
- : ctx.stmts.getRowid.get(id);
222
-
223
- if (!rowidResult || rowidResult.rowid == null) {
224
- throw new Error(
225
- `Could not find rowid for entry: ${wasUpdate ? `file_path=${filePath}` : `id=${id}`}`,
226
- );
227
- }
228
-
229
- const rowid = Number(rowidResult.rowid);
230
- if (!Number.isFinite(rowid) || rowid < 1) {
231
- throw new Error(
232
- `Invalid rowid retrieved: ${rowidResult.rowid} (type: ${typeof rowidResult.rowid})`,
233
- );
234
- }
235
-
236
- // Skip embedding generation for event entries — they are excluded from
237
- // default semantic search and don't need vector representations
238
- if (cat !== "event") {
239
- const embeddingText = [title, body].filter(Boolean).join(" ");
240
- const embedding = await ctx.embed(embeddingText);
241
-
242
- if (embedding) {
243
- try {
244
- ctx.deleteVec(rowid);
245
- } catch {
246
- /* no-op if not found */
247
- }
248
- ctx.insertVec(rowid, embedding);
249
- }
250
- }
251
- }
252
-
253
- /**
254
- * Prune expired entries: delete files, vec rows, and DB rows for all entries
255
- * where expires_at <= now(). Safe to call on startup or CLI — non-destructive
256
- * to active data.
257
- *
258
- * @param {import('../server/types.js').BaseCtx} ctx
259
- * @returns {Promise<number>} count of pruned entries
260
- */
261
- export async function pruneExpired(ctx) {
262
- const expired = ctx.db
263
- .prepare(
264
- "SELECT id, file_path FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
265
- )
266
- .all();
267
-
268
- for (const row of expired) {
269
- if (row.file_path) {
270
- try {
271
- unlinkSync(row.file_path);
272
- } catch {}
273
- }
274
- const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
275
- if (vRowid) {
276
- try {
277
- ctx.deleteVec(Number(vRowid));
278
- } catch {}
279
- }
280
- ctx.stmts.deleteEntry.run(row.id);
281
- }
282
-
283
- return expired.length;
284
- }
285
-
286
- /**
287
- * Bulk reindex: sync vault directory into the database.
288
- * P2: Wrapped in a transaction for atomicity.
289
- * P3: Detects title/tag/meta changes, not just body.
290
- * P4: Batches embedding calls for performance.
291
- *
292
- * @param {import('../server/types.js').BaseCtx} ctx
293
- * @param {{ fullSync?: boolean }} opts — fullSync=true adds/updates/deletes; false=add-only
294
- * @returns {Promise<{added: number, updated: number, removed: number, unchanged: number}>}
295
- */
296
- export async function reindex(ctx, opts = {}) {
297
- const { fullSync = true } = opts;
298
- const stats = { added: 0, updated: 0, removed: 0, unchanged: 0 };
299
-
300
- if (!existsSync(ctx.config.vaultDir)) return stats;
301
-
302
- // Use INSERT OR IGNORE for reindex — handles files with duplicate frontmatter IDs.
303
- // Local mode: no user_id column (15 params).
304
- // Hosted mode: user_id is NULL for file-sourced entries (14 params, NULL literal).
305
- const isLocalReindex = ctx.stmts._mode === "local";
306
- const upsertEntry = ctx.db.prepare(
307
- isLocalReindex
308
- ? `INSERT OR IGNORE INTO vault (id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
309
- : `INSERT OR IGNORE INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
310
- );
311
-
312
- // Auto-discover kind directories, supporting both:
313
- // - Nested: knowledge/insights/, events/sessions/ (category dirs at top level)
314
- // - Flat: insights/, decisions/ (legacy — kind dirs at top level)
315
- const kindEntries = []; // { kind, dir }
316
- const topDirs = readdirSync(ctx.config.vaultDir, {
317
- withFileTypes: true,
318
- }).filter(
319
- (d) =>
320
- d.isDirectory() && !EXCLUDED_DIRS.has(d.name) && !d.name.startsWith("_"),
321
- );
322
-
323
- for (const d of topDirs) {
324
- if (CATEGORY_DIRS.has(d.name)) {
325
- // Category directory — look one level deeper for kind directories
326
- const catDir = join(ctx.config.vaultDir, d.name);
327
- const subDirs = readdirSync(catDir, { withFileTypes: true }).filter(
328
- (sd) => sd.isDirectory() && !sd.name.startsWith("_"),
329
- );
330
- for (const sd of subDirs) {
331
- kindEntries.push({
332
- kind: dirToKind(sd.name),
333
- dir: join(catDir, sd.name),
334
- });
335
- }
336
- } else {
337
- // Legacy flat structure — top-level dir is a kind dir
338
- kindEntries.push({
339
- kind: dirToKind(d.name),
340
- dir: join(ctx.config.vaultDir, d.name),
341
- });
342
- }
343
- }
344
-
345
- // Phase 1: Sync DB ops in a transaction — FTS is searchable immediately after COMMIT.
346
- // Phase 2: Async embedding runs post-transaction so it can't hold the write lock
347
- // or roll back DB state on failure.
348
- const pendingEmbeds = []; // { rowid, text, isUpdate }
349
-
350
- ctx.db.exec("BEGIN");
351
- try {
352
- for (const { kind, dir } of kindEntries) {
353
- const category = categoryFor(kind);
354
- const mdFiles = walkDir(dir).filter(
355
- (f) => !EXCLUDED_FILES.has(basename(f.filePath)),
356
- );
357
-
358
- // P3: Fetch all mutable fields for change detection
359
- const dbRows = ctx.db
360
- .prepare(
361
- "SELECT id, file_path, body, title, tags, meta, related_to FROM vault WHERE kind = ?",
362
- )
363
- .all(kind);
364
- const dbByPath = new Map(dbRows.map((r) => [r.file_path, r]));
365
- const diskPaths = new Set(mdFiles.map((e) => e.filePath));
366
-
367
- for (const { filePath, relDir } of mdFiles) {
368
- const existing = dbByPath.get(filePath);
369
-
370
- // In add-only mode, skip files already in DB
371
- if (!fullSync && existing) {
372
- stats.unchanged++;
373
- continue;
374
- }
375
-
376
- const raw = readFileSync(filePath, "utf-8");
377
- if (!raw.startsWith("---\n")) {
378
- console.error(`[reindex] skipping (no frontmatter): ${filePath}`);
379
- continue;
380
- }
381
- const { meta: fmMeta, body: rawBody } = parseFrontmatter(raw);
382
- const parsed = parseEntryFromMarkdown(kind, rawBody, fmMeta);
383
-
384
- // Extract identity_key and expires_at from frontmatter
385
- const identity_key = fmMeta.identity_key || null;
386
- const expires_at = fmMeta.expires_at || null;
387
- const related_to = Array.isArray(fmMeta.related_to)
388
- ? fmMeta.related_to
389
- : null;
390
- const relatedToJson = related_to?.length
391
- ? JSON.stringify(related_to)
392
- : null;
393
-
394
- // Derive folder from disk location (source of truth)
395
- const meta = { ...(parsed.meta || {}) };
396
- if (relDir) meta.folder = relDir;
397
- else delete meta.folder;
398
- const metaJson = Object.keys(meta).length ? JSON.stringify(meta) : null;
399
-
400
- if (!existing) {
401
- // New file — add to DB (OR IGNORE if ID already exists at another path)
402
- const id = fmMeta.id || ulid();
403
- const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
404
- const created = fmMeta.created || new Date().toISOString();
405
-
406
- const result = upsertEntry.run(
407
- id,
408
- kind,
409
- category,
410
- parsed.title || null,
411
- parsed.body,
412
- metaJson,
413
- tagsJson,
414
- fmMeta.source || "file",
415
- filePath,
416
- identity_key,
417
- expires_at,
418
- created,
419
- fmMeta.updated || created,
420
- );
421
- if (result.changes > 0) {
422
- if (relatedToJson && ctx.stmts.updateRelatedTo) {
423
- ctx.stmts.updateRelatedTo.run(relatedToJson, id);
424
- }
425
- if (category !== "event") {
426
- const rowidResult = ctx.stmts.getRowid.get(id);
427
- if (rowidResult?.rowid) {
428
- const embeddingText = [parsed.title, parsed.body]
429
- .filter(Boolean)
430
- .join(" ");
431
- pendingEmbeds.push({
432
- rowid: rowidResult.rowid,
433
- text: embeddingText,
434
- });
435
- }
436
- }
437
- stats.added++;
438
- } else {
439
- stats.unchanged++;
440
- }
441
- } else if (fullSync) {
442
- // P3: Compare all mutable fields, not just body
443
- const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
444
- const titleChanged =
445
- (parsed.title || null) !== (existing.title || null);
446
- const bodyChanged = existing.body !== parsed.body;
447
- const tagsChanged = tagsJson !== (existing.tags || null);
448
- const metaChanged = metaJson !== (existing.meta || null);
449
- const relatedToChanged =
450
- relatedToJson !== (existing.related_to || null);
451
-
452
- if (
453
- bodyChanged ||
454
- titleChanged ||
455
- tagsChanged ||
456
- metaChanged ||
457
- relatedToChanged
458
- ) {
459
- ctx.stmts.updateEntry.run(
460
- parsed.title || null,
461
- parsed.body,
462
- metaJson,
463
- tagsJson,
464
- fmMeta.source || "file",
465
- category,
466
- identity_key,
467
- expires_at,
468
- filePath,
469
- );
470
- if (relatedToChanged && ctx.stmts.updateRelatedTo) {
471
- ctx.stmts.updateRelatedTo.run(relatedToJson, existing.id);
472
- }
473
-
474
- // Queue re-embed if title or body changed (vector ops deferred to Phase 2)
475
- if ((bodyChanged || titleChanged) && category !== "event") {
476
- const rowid = ctx.stmts.getRowid.get(existing.id)?.rowid;
477
- if (rowid) {
478
- const embeddingText = [parsed.title, parsed.body]
479
- .filter(Boolean)
480
- .join(" ");
481
- pendingEmbeds.push({
482
- rowid,
483
- text: embeddingText,
484
- isUpdate: true,
485
- });
486
- }
487
- }
488
- stats.updated++;
489
- } else {
490
- stats.unchanged++;
491
- }
492
- } else {
493
- stats.unchanged++;
494
- }
495
- }
496
-
497
- // Find deleted files (in DB but not on disk) — only in fullSync mode
498
- if (fullSync) {
499
- for (const [dbPath, row] of dbByPath) {
500
- if (!diskPaths.has(dbPath)) {
501
- const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
502
- if (vRowid) {
503
- try {
504
- ctx.deleteVec(vRowid);
505
- } catch {}
506
- }
507
- ctx.stmts.deleteEntry.run(row.id);
508
- stats.removed++;
509
- }
510
- }
511
- }
512
- }
513
-
514
- // Clean up entries for kinds whose directories no longer exist on disk
515
- if (fullSync) {
516
- const indexedKinds = new Set(kindEntries.map((ke) => ke.kind));
517
- const allDbKinds = ctx.db
518
- .prepare("SELECT DISTINCT kind FROM vault")
519
- .all();
520
- for (const { kind } of allDbKinds) {
521
- if (!indexedKinds.has(kind)) {
522
- const orphaned = ctx.db
523
- .prepare("SELECT id, rowid FROM vault WHERE kind = ?")
524
- .all(kind);
525
- for (const row of orphaned) {
526
- try {
527
- ctx.deleteVec(row.rowid);
528
- } catch {}
529
- ctx.stmts.deleteEntry.run(row.id);
530
- stats.removed++;
531
- }
532
- }
533
- }
534
- }
535
-
536
- // Prune expired entries
537
- const expired = ctx.db
538
- .prepare(
539
- "SELECT id, file_path FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
540
- )
541
- .all();
542
-
543
- for (const row of expired) {
544
- if (row.file_path) {
545
- try {
546
- unlinkSync(row.file_path);
547
- } catch {}
548
- }
549
- const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
550
- if (vRowid) {
551
- try {
552
- ctx.deleteVec(vRowid);
553
- } catch {}
554
- }
555
- ctx.stmts.deleteEntry.run(row.id);
556
- stats.removed++;
557
- }
558
-
559
- ctx.db.exec("COMMIT");
560
- } catch (e) {
561
- ctx.db.exec("ROLLBACK");
562
- throw e;
563
- }
564
-
565
- // Phase 2: Async embedding — runs after COMMIT so FTS is already searchable.
566
- // Failures here are non-fatal; semantic search catches up on next reindex.
567
- // Vec delete happens atomically with insert (only on success) to avoid
568
- // leaving entries permanently without vectors if embedBatch() fails mid-batch.
569
- for (let i = 0; i < pendingEmbeds.length; i += EMBED_BATCH_SIZE) {
570
- const batch = pendingEmbeds.slice(i, i + EMBED_BATCH_SIZE);
571
- const embeddings = await embedBatch(batch.map((e) => e.text));
572
- for (let j = 0; j < batch.length; j++) {
573
- if (embeddings[j]) {
574
- try {
575
- ctx.deleteVec(batch[j].rowid);
576
- } catch {}
577
- ctx.insertVec(batch[j].rowid, embeddings[j]);
578
- }
579
- }
580
- }
581
-
582
- return stats;
583
- }
package/src/index.js DELETED
@@ -1,71 +0,0 @@
1
- /**
2
- * @context-vault/core — Shared core for context-vault
3
- *
4
- * Re-exports all public APIs from capture, index, retrieve, server, and core layers.
5
- */
6
-
7
- // Core utilities
8
- export {
9
- categoryFor,
10
- categoryDirFor,
11
- defaultTierFor,
12
- CATEGORY_DIRS,
13
- } from "./core/categories.js";
14
- export { parseArgs, resolveConfig } from "./core/config.js";
15
- export {
16
- ulid,
17
- slugify,
18
- kindToDir,
19
- dirToKind,
20
- normalizeKind,
21
- kindToPath,
22
- safeJoin,
23
- walkDir,
24
- } from "./core/files.js";
25
- export {
26
- formatFrontmatter,
27
- parseFrontmatter,
28
- extractCustomMeta,
29
- parseEntryFromMarkdown,
30
- } from "./core/frontmatter.js";
31
- export { gatherVaultStatus } from "./core/status.js";
32
- export {
33
- PLURAL_TO_SINGULAR,
34
- planMigration,
35
- executeMigration,
36
- } from "./core/migrate-dirs.js";
37
-
38
- // Capture layer
39
- export {
40
- writeEntry,
41
- updateEntryFile,
42
- captureAndIndex,
43
- } from "./capture/index.js";
44
- export { writeEntryFile } from "./capture/file-ops.js";
45
- export { formatBody } from "./capture/formatters.js";
46
-
47
- // Index layer
48
- export {
49
- SCHEMA_DDL,
50
- initDatabase,
51
- prepareStatements,
52
- insertVec,
53
- deleteVec,
54
- } from "./index/db.js";
55
- export { embed, embedBatch, resetEmbedPipeline } from "./index/embed.js";
56
- export { indexEntry, reindex, pruneExpired } from "./index/index.js";
57
-
58
- // Retrieve layer
59
- export { hybridSearch } from "./retrieve/index.js";
60
-
61
- // Consolidation utilities
62
- export { findHotTags, findColdEntries } from "./consolidation/index.js";
63
-
64
- // Server tools & helpers
65
- export { registerTools } from "./server/tools.js";
66
- export {
67
- ok,
68
- err,
69
- ensureVaultExists,
70
- ensureValidKind,
71
- } from "./server/helpers.js";
@@ -1,44 +0,0 @@
1
- /**
2
- * helpers.js — Shared MCP response helpers and validation
3
- */
4
-
5
- import pkg from "../../package.json" with { type: "json" };
6
-
7
- export function ok(text) {
8
- return { content: [{ type: "text", text }] };
9
- }
10
-
11
- export function err(text, code = "UNKNOWN", meta = {}) {
12
- return {
13
- content: [{ type: "text", text }],
14
- isError: true,
15
- code,
16
- _meta: {
17
- cv_version: pkg.version,
18
- node_version: process.version,
19
- platform: process.platform,
20
- arch: process.arch,
21
- ...meta,
22
- },
23
- };
24
- }
25
-
26
- export function ensureVaultExists(config) {
27
- if (!config.vaultDirExists) {
28
- return err(
29
- `Vault directory not found: ${config.vaultDir}. Run context-status for diagnostics.`,
30
- "VAULT_NOT_FOUND",
31
- );
32
- }
33
- return null;
34
- }
35
-
36
- export function ensureValidKind(kind) {
37
- if (!/^[a-z][a-z0-9_-]*$/.test(kind)) {
38
- return err(
39
- "Required: kind (lowercase alphanumeric, e.g. 'insight', 'reference')",
40
- "INVALID_KIND",
41
- );
42
- }
43
- return null;
44
- }