@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,48 +1,21 @@
1
- /**
2
- * Retrieve Layer — Public API
3
- *
4
- * All read-path query logic: hybrid semantic search and any future
5
- * query patterns (scoped, recency-weighted, etc.).
6
- *
7
- * Agent Constraint: Read-only access to DB. Never writes.
8
- */
1
+ import type { BaseCtx, SearchResult, SearchOptions, VaultEntry } from "./types.js";
9
2
 
10
3
  const NEAR_DUP_THRESHOLD = 0.92;
11
-
12
4
  const RRF_K = 60;
13
5
 
14
- /**
15
- * Exponential recency decay score based on updated_at timestamp.
16
- * Returns e^(-decayRate * ageDays) for valid dates, or 0.5 as a neutral
17
- * score when updatedAt is null/undefined.
18
- *
19
- * @param {string|null|undefined} updatedAt - ISO timestamp
20
- * @param {number} decayRate - Decay rate per day (default 0.05)
21
- * @returns {number} Score in [0, 1]
22
- */
23
- export function recencyDecayScore(updatedAt, decayRate = 0.05) {
6
+ export function recencyDecayScore(updatedAt: string | null | undefined, decayRate = 0.05): number {
24
7
  if (updatedAt == null) return 0.5;
25
8
  const ageDays = (Date.now() - new Date(updatedAt).getTime()) / 86400000;
26
9
  return Math.exp(-decayRate * ageDays);
27
10
  }
28
11
 
29
- /**
30
- * Dot product of two Float32Array vectors (cosine similarity for unit vectors).
31
- */
32
- export function dotProduct(a, b) {
12
+ export function dotProduct(a: Float32Array, b: Float32Array): number {
33
13
  let sum = 0;
34
14
  for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
35
15
  return sum;
36
16
  }
37
17
 
38
- /**
39
- * Build a tiered FTS5 query that prioritises phrase match, then proximity,
40
- * then AND. Multi-word queries become:
41
- * "word1 word2" OR NEAR("word1" "word2", 10) OR "word1" AND "word2"
42
- * Single-word queries remain a simple quoted term.
43
- * Returns null if no valid words remain after stripping FTS5 metacharacters.
44
- */
45
- export function buildFtsQuery(query) {
18
+ export function buildFtsQuery(query: string): string | null {
46
19
  const words = query
47
20
  .split(/[\s-]+/)
48
21
  .map((w) => w.replace(/[*"():^~{}]/g, ""))
@@ -55,40 +28,27 @@ export function buildFtsQuery(query) {
55
28
  return `${phrase} OR ${near} OR ${and}`;
56
29
  }
57
30
 
58
- /**
59
- * Category-aware recency decay:
60
- * knowledge + entity: no decay (enduring)
61
- * event: steeper decay (~0.5 at 30 days)
62
- */
63
- export function recencyBoost(createdAt, category, decayDays = 30) {
31
+ export function recencyBoost(createdAt: string, category: string, decayDays = 30): number {
64
32
  if (category !== "event") return 1.0;
65
33
  const ageDays = (Date.now() - new Date(createdAt).getTime()) / 86400000;
66
34
  return 1 / (1 + ageDays / decayDays);
67
35
  }
68
36
 
69
- /**
70
- * Build additional WHERE clauses for category/time filtering.
71
- * Returns { clauses: string[], params: any[] }
72
- */
73
37
  export function buildFilterClauses({
74
38
  categoryFilter,
75
39
  excludeEvents = false,
76
40
  since,
77
41
  until,
78
- userIdFilter,
79
- teamIdFilter,
80
42
  includeSuperseeded = false,
81
- }) {
82
- const clauses = [];
83
- const params = [];
84
- if (userIdFilter !== undefined) {
85
- clauses.push("e.user_id = ?");
86
- params.push(userIdFilter);
87
- }
88
- if (teamIdFilter) {
89
- clauses.push("e.team_id = ?");
90
- params.push(teamIdFilter);
91
- }
43
+ }: {
44
+ categoryFilter?: string | null;
45
+ excludeEvents?: boolean;
46
+ since?: string | null;
47
+ until?: string | null;
48
+ includeSuperseeded?: boolean;
49
+ }): { clauses: string[]; params: unknown[] } {
50
+ const clauses: string[] = [];
51
+ const params: unknown[] = [];
92
52
  if (categoryFilter) {
93
53
  clauses.push("e.category = ?");
94
54
  params.push(categoryFilter);
@@ -111,16 +71,11 @@ export function buildFilterClauses({
111
71
  return { clauses, params };
112
72
  }
113
73
 
114
- /**
115
- * Reciprocal Rank Fusion: merge multiple ranked lists into a single score.
116
- * Each document receives 1/(k + rank) from each list it appears in.
117
- *
118
- * @param {Array<string[]>} rankedLists - Arrays of document IDs in rank order (best first).
119
- * @param {number} k - Smoothing constant (default RRF_K = 60).
120
- * @returns {Map<string, number>} Map of id -> RRF score.
121
- */
122
- export function reciprocalRankFusion(rankedLists, k = RRF_K) {
123
- const scores = new Map();
74
+ export function reciprocalRankFusion(
75
+ rankedLists: string[][],
76
+ k: number = RRF_K,
77
+ ): Map<string, number> {
78
+ const scores = new Map<string, number>();
124
79
  for (const list of rankedLists) {
125
80
  for (let rank = 0; rank < list.length; rank++) {
126
81
  const id = list[rank];
@@ -130,26 +85,12 @@ export function reciprocalRankFusion(rankedLists, k = RRF_K) {
130
85
  return scores;
131
86
  }
132
87
 
133
- /**
134
- * Hybrid search combining FTS5 text matching and vector similarity,
135
- * with RRF merging, recency decay, and near-duplicate suppression.
136
- *
137
- * Pipeline:
138
- * 1. FTS5 ranked list
139
- * 2. Vector (semantic) ranked list
140
- * 3. RRF: merge the two ranked lists into a single score
141
- * 4. Recency decay: penalise old events (knowledge/entity entries unaffected)
142
- * 5. Near-duplicate suppression (cosine similarity > 0.92 threshold)
143
- *
144
- * @param {import('../server/types.js').BaseCtx} ctx
145
- * @param {string} query
146
- * @param {{ kindFilter?: string|null, categoryFilter?: string|null, since?: string|null, until?: string|null, limit?: number, offset?: number }} opts
147
- * @returns {Promise<Array<{id, kind, category, title, body, meta, tags, source, file_path, created_at, score}>>}
148
- */
149
88
  export async function hybridSearch(
150
- ctx,
151
- query,
152
- {
89
+ ctx: BaseCtx,
90
+ query: string,
91
+ opts: SearchOptions = {},
92
+ ): Promise<SearchResult[]> {
93
+ const {
153
94
  kindFilter = null,
154
95
  categoryFilter = null,
155
96
  excludeEvents = false,
@@ -158,33 +99,28 @@ export async function hybridSearch(
158
99
  limit = 20,
159
100
  offset = 0,
160
101
  decayDays = 30,
161
- userIdFilter,
162
- teamIdFilter = null,
163
102
  includeSuperseeded = false,
164
- } = {},
165
- ) {
166
- const rowMap = new Map();
167
- const idToRowid = new Map();
168
- let queryVec = null;
103
+ } = opts;
104
+
105
+ const rowMap = new Map<string, VaultEntry>();
106
+ const idToRowid = new Map<string, number>();
107
+ let queryVec: Float32Array | null = null;
169
108
 
170
109
  const extraFilters = buildFilterClauses({
171
110
  categoryFilter,
172
111
  excludeEvents,
173
112
  since,
174
113
  until,
175
- userIdFilter,
176
- teamIdFilter,
177
114
  includeSuperseeded,
178
115
  });
179
116
 
180
- const ftsRankedIds = [];
117
+ const ftsRankedIds: string[] = [];
181
118
 
182
- // Stage 1a: FTS5 — collect ranked list of IDs
183
119
  const ftsQuery = buildFtsQuery(query);
184
120
  if (ftsQuery) {
185
121
  try {
186
122
  const whereParts = ["vault_fts MATCH ?"];
187
- const ftsParams = [ftsQuery];
123
+ const ftsParams: unknown[] = [ftsQuery];
188
124
 
189
125
  if (kindFilter) {
190
126
  whereParts.push("e.kind = ?");
@@ -194,43 +130,34 @@ export async function hybridSearch(
194
130
  ftsParams.push(...extraFilters.params);
195
131
 
196
132
  const ftsSQL = `SELECT e.*, rank FROM vault_fts f JOIN vault e ON f.rowid = e.rowid WHERE ${whereParts.join(" AND ")} ORDER BY rank LIMIT 15`;
197
- const rows = ctx.db.prepare(ftsSQL).all(...ftsParams);
133
+ // @ts-expect-error -- node:sqlite types are overly strict for dynamic SQL params
134
+ const rows = ctx.db.prepare(ftsSQL).all(...ftsParams) as unknown as (VaultEntry & { rank: number })[];
198
135
 
199
136
  for (const { rank: _rank, ...row } of rows) {
200
137
  ftsRankedIds.push(row.id);
201
138
  if (!rowMap.has(row.id)) rowMap.set(row.id, row);
202
139
  }
203
140
  } catch (err) {
204
- if (!err.message?.includes("fts5: syntax error")) {
205
- console.error(`[retrieve] FTS search error: ${err.message}`);
141
+ if (!(err as Error).message?.includes("fts5: syntax error")) {
142
+ console.error(`[retrieve] FTS search error: ${(err as Error).message}`);
206
143
  }
207
144
  }
208
145
  }
209
146
 
210
- const vecRankedIds = [];
211
- const vecSimMap = new Map();
147
+ const vecRankedIds: string[] = [];
148
+ const vecSimMap = new Map<string, number>();
212
149
 
213
- // Stage 1b: Vector similarity — collect ranked list of IDs and raw similarity scores
214
150
  try {
215
- const vecCount = ctx.db
216
- .prepare("SELECT COUNT(*) as c FROM vault_vec")
217
- .get().c;
151
+ const vecCount = (ctx.db.prepare("SELECT COUNT(*) as c FROM vault_vec").get() as { c: number }).c;
218
152
  if (vecCount > 0) {
219
153
  queryVec = await ctx.embed(query);
220
154
  if (queryVec) {
221
- const hasPostFilter = userIdFilter !== undefined || teamIdFilter;
222
- const vecLimit = hasPostFilter
223
- ? kindFilter
224
- ? 60
225
- : 30
226
- : kindFilter
227
- ? 30
228
- : 15;
155
+ const vecLimit = kindFilter ? 30 : 15;
229
156
  const vecRows = ctx.db
230
157
  .prepare(
231
158
  `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
232
159
  )
233
- .all(queryVec, vecLimit);
160
+ .all(queryVec, vecLimit) as { rowid: number; distance: number }[];
234
161
 
235
162
  if (vecRows.length) {
236
163
  const rowids = vecRows.map((vr) => vr.rowid);
@@ -239,17 +166,14 @@ export async function hybridSearch(
239
166
  .prepare(
240
167
  `SELECT rowid, * FROM vault WHERE rowid IN (${placeholders})`,
241
168
  )
242
- .all(...rowids);
169
+ .all(...rowids) as unknown as (VaultEntry & { rowid: number })[];
243
170
 
244
- const byRowid = new Map();
171
+ const byRowid = new Map<number, VaultEntry & { rowid: number }>();
245
172
  for (const row of hydrated) byRowid.set(row.rowid, row);
246
173
 
247
174
  for (const vr of vecRows) {
248
175
  const row = byRowid.get(vr.rowid);
249
176
  if (!row) continue;
250
- if (userIdFilter !== undefined && row.user_id !== userIdFilter)
251
- continue;
252
- if (teamIdFilter && row.team_id !== teamIdFilter) continue;
253
177
  if (kindFilter && row.kind !== kindFilter) continue;
254
178
  if (categoryFilter && row.category !== categoryFilter) continue;
255
179
  if (excludeEvents && row.category === "event") continue;
@@ -261,8 +185,6 @@ export async function hybridSearch(
261
185
  const { rowid: _rowid, ...cleanRow } = row;
262
186
  idToRowid.set(cleanRow.id, Number(row.rowid));
263
187
 
264
- // sqlite-vec returns L2 distance [0, 2] for normalized vectors.
265
- // Convert to similarity [0, 1]: 1 - distance/2
266
188
  const vecSim = Math.max(0, 1 - vr.distance / 2);
267
189
  vecSimMap.set(cleanRow.id, vecSim);
268
190
  vecRankedIds.push(cleanRow.id);
@@ -273,33 +195,29 @@ export async function hybridSearch(
273
195
  }
274
196
  }
275
197
  } catch (err) {
276
- if (!err.message?.includes("no such table")) {
277
- console.error(`[retrieve] Vector search error: ${err.message}`);
198
+ if (!(err as Error).message?.includes("no such table")) {
199
+ console.error(`[retrieve] Vector search error: ${(err as Error).message}`);
278
200
  }
279
201
  }
280
202
 
281
203
  if (rowMap.size === 0) return [];
282
204
 
283
- // Stage 2: RRF — merge FTS and vector ranked lists into a single score
284
205
  const rrfScores = reciprocalRankFusion([ftsRankedIds, vecRankedIds]);
285
206
 
286
- // Stage 3: Apply category-aware recency boost to RRF scores
287
207
  for (const [id, entry] of rowMap) {
288
208
  const boost = recencyBoost(entry.created_at, entry.category, decayDays);
289
209
  rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost);
290
210
  }
291
211
 
292
- // Attach final score to each entry and sort by RRF score descending
293
- const candidates = [...rowMap.values()].map((entry) => ({
212
+ const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
294
213
  ...entry,
295
214
  score: rrfScores.get(entry.id) ?? 0,
296
215
  }));
297
216
  candidates.sort((a, b) => b.score - a.score);
298
217
 
299
- // Stage 4: Fetch embeddings for near-duplicate suppression
300
- const embeddingMap = new Map();
218
+ const embeddingMap = new Map<string, Float32Array>();
301
219
  if (queryVec && idToRowid.size > 0) {
302
- const rowidToId = new Map();
220
+ const rowidToId = new Map<number, string>();
303
221
  for (const [id, rowid] of idToRowid) rowidToId.set(rowid, id);
304
222
 
305
223
  const rowidsToFetch = [...idToRowid.values()];
@@ -309,7 +227,7 @@ export async function hybridSearch(
309
227
  .prepare(
310
228
  `SELECT rowid, embedding FROM vault_vec WHERE rowid IN (${placeholders})`,
311
229
  )
312
- .all(...rowidsToFetch);
230
+ .all(...rowidsToFetch) as { rowid: number; embedding: Buffer }[];
313
231
  for (const row of vecData) {
314
232
  const id = rowidToId.get(Number(row.rowid));
315
233
  const buf = row.embedding;
@@ -320,15 +238,14 @@ export async function hybridSearch(
320
238
  );
321
239
  }
322
240
  }
323
- } catch (_) {
324
- // Embeddings unavailable — near-dup suppression skipped
241
+ } catch {
242
+ // Embeddings unavailable
325
243
  }
326
244
  }
327
245
 
328
- // Stage 5: Near-duplicate suppression (cosine similarity > 0.92 threshold)
329
246
  if (queryVec && embeddingMap.size > 0) {
330
- const selected = [];
331
- const selectedVecs = [];
247
+ const selected: SearchResult[] = [];
248
+ const selectedVecs: Float32Array[] = [];
332
249
  for (const candidate of candidates) {
333
250
  if (selected.length >= offset + limit) break;
334
251
  const vec = embeddingMap.get(candidate.id);
@@ -344,30 +261,25 @@ export async function hybridSearch(
344
261
  if (vec) selectedVecs.push(vec);
345
262
  }
346
263
  const dedupedPage = selected.slice(offset, offset + limit);
347
- trackAccess(ctx.db, dedupedPage);
264
+ trackAccess(ctx, dedupedPage);
348
265
  return dedupedPage;
349
266
  }
350
267
 
351
268
  const finalPage = candidates.slice(offset, offset + limit);
352
- trackAccess(ctx.db, finalPage);
269
+ trackAccess(ctx, finalPage);
353
270
  return finalPage;
354
271
  }
355
272
 
356
- /**
357
- * Increment hit_count and set last_accessed_at for a batch of retrieved entries.
358
- * Single batched UPDATE for efficiency.
359
- *
360
- * @param {import('node:sqlite').DatabaseSync} db
361
- * @param {Array<{id: string}>} entries
362
- */
363
- function trackAccess(db, entries) {
273
+ function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
364
274
  if (!entries.length) return;
365
275
  try {
366
276
  const placeholders = entries.map(() => "?").join(",");
367
- db.prepare(
368
- `UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE id IN (${placeholders})`,
369
- ).run(...entries.map((e) => e.id));
370
- } catch (_) {
371
- // Non-fatal: frequency tracking is best-effort
277
+ ctx.db
278
+ .prepare(
279
+ `UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE id IN (${placeholders})`,
280
+ )
281
+ .run(...entries.map((e) => e.id));
282
+ } catch {
283
+ // Non-fatal
372
284
  }
373
285
  }
package/src/types.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { DatabaseSync, StatementSync } from "node:sqlite";
2
+
3
+ export interface VaultConfig {
4
+ vaultDir: string;
5
+ dataDir: string;
6
+ dbPath: string;
7
+ devDir: string;
8
+ eventDecayDays: number;
9
+ thresholds: GrowthThresholds;
10
+ telemetry: boolean;
11
+ resolvedFrom: string;
12
+ configPath?: string;
13
+ vaultDirExists?: boolean;
14
+ recall: RecallConfig;
15
+ consolidation: ConsolidationConfig;
16
+ lifecycle: Record<string, { archiveAfterDays?: number }>;
17
+ }
18
+
19
+ export interface RecallConfig {
20
+ maxResults: number;
21
+ maxOutputBytes: number;
22
+ minRelevanceScore: number;
23
+ excludeKinds: string[];
24
+ excludeCategories: string[];
25
+ bodyTruncateChars: number;
26
+ }
27
+
28
+ export interface ConsolidationConfig {
29
+ tagThreshold: number;
30
+ maxAgeDays: number;
31
+ autoConsolidate: boolean;
32
+ }
33
+
34
+ export interface GrowthThresholds {
35
+ totalEntries: { warn: number; critical: number };
36
+ eventEntries: { warn: number; critical: number };
37
+ vaultSizeBytes: { warn: number; critical: number };
38
+ eventsWithoutTtl: { warn: number };
39
+ }
40
+
41
+ export interface PreparedStatements {
42
+ insertEntry: StatementSync;
43
+ updateEntry: StatementSync;
44
+ deleteEntry: StatementSync;
45
+ getRowid: StatementSync;
46
+ getRowidByPath: StatementSync;
47
+ getEntryById: StatementSync;
48
+ getByIdentityKey: StatementSync;
49
+ upsertByIdentityKey: StatementSync;
50
+ updateSourceFiles: StatementSync;
51
+ updateRelatedTo: StatementSync;
52
+ insertVecStmt: StatementSync;
53
+ deleteVecStmt: StatementSync;
54
+ updateSupersededBy: StatementSync;
55
+ clearSupersededByRef: StatementSync;
56
+ }
57
+
58
+ export interface VaultEntry {
59
+ id: string;
60
+ kind: string;
61
+ category: string;
62
+ title: string | null;
63
+ body: string;
64
+ meta: string | null;
65
+ tags: string | null;
66
+ source: string | null;
67
+ file_path: string | null;
68
+ identity_key: string | null;
69
+ expires_at: string | null;
70
+ superseded_by: string | null;
71
+ created_at: string;
72
+ updated_at: string | null;
73
+ hit_count: number;
74
+ last_accessed_at: string | null;
75
+ source_files: string | null;
76
+ tier: string;
77
+ related_to: string | null;
78
+ rowid?: number;
79
+ }
80
+
81
+ export interface SearchResult extends VaultEntry {
82
+ score: number;
83
+ stale?: boolean;
84
+ stale_reason?: string;
85
+ }
86
+
87
+ export interface CaptureInput {
88
+ kind: string;
89
+ title?: string | null;
90
+ body: string;
91
+ meta?: Record<string, unknown> | null;
92
+ tags?: string[] | null;
93
+ source?: string | null;
94
+ folder?: string | null;
95
+ identity_key?: string | null;
96
+ expires_at?: string | null;
97
+ supersedes?: string[] | null;
98
+ related_to?: string[] | null;
99
+ source_files?: Array<{ path: string; hash: string }> | null;
100
+ tier?: string | null;
101
+ }
102
+
103
+ export interface CaptureResult {
104
+ id: string;
105
+ filePath: string;
106
+ kind: string;
107
+ category: string;
108
+ title: string | null;
109
+ body: string;
110
+ meta: Record<string, unknown> | undefined;
111
+ tags: string[] | null;
112
+ source: string | null;
113
+ createdAt: string;
114
+ updatedAt: string;
115
+ identity_key: string | null;
116
+ expires_at: string | null;
117
+ supersedes: string[] | null;
118
+ related_to: string[] | null;
119
+ source_files: Array<{ path: string; hash: string }> | null;
120
+ tier: string | null;
121
+ }
122
+
123
+ export interface IndexEntryInput {
124
+ id: string;
125
+ kind: string;
126
+ category: string;
127
+ title: string | null;
128
+ body: string;
129
+ meta: Record<string, unknown> | undefined;
130
+ tags: string[] | null;
131
+ source: string | null;
132
+ filePath: string;
133
+ createdAt: string;
134
+ identity_key: string | null;
135
+ expires_at: string | null;
136
+ source_files: Array<{ path: string; hash: string }> | null;
137
+ tier: string | null;
138
+ }
139
+
140
+ export interface ReindexStats {
141
+ added: number;
142
+ updated: number;
143
+ removed: number;
144
+ unchanged: number;
145
+ }
146
+
147
+ export interface BaseCtx {
148
+ db: DatabaseSync;
149
+ config: VaultConfig;
150
+ stmts: PreparedStatements;
151
+ embed: (text: string) => Promise<Float32Array | null>;
152
+ insertVec: (rowid: number, embedding: Float32Array) => void;
153
+ deleteVec: (rowid: number) => void;
154
+ }
155
+
156
+ export interface SearchOptions {
157
+ kindFilter?: string | null;
158
+ categoryFilter?: string | null;
159
+ excludeEvents?: boolean;
160
+ since?: string | null;
161
+ until?: string | null;
162
+ limit?: number;
163
+ offset?: number;
164
+ decayDays?: number;
165
+ includeSuperseeded?: boolean;
166
+ }
@@ -1,99 +0,0 @@
1
- /**
2
- * file-ops.js — Capture-specific file operations
3
- *
4
- * Writes markdown entry files with frontmatter to the vault directory.
5
- */
6
-
7
- import { mkdirSync, writeFileSync } from "node:fs";
8
- import { resolve, relative } from "node:path";
9
- import { formatFrontmatter } from "../core/frontmatter.js";
10
- import { slugify, kindToPath } from "../core/files.js";
11
- import { formatBody } from "./formatters.js";
12
-
13
- export function safeFolderPath(vaultDir, kind, folder) {
14
- const base = resolve(vaultDir, kindToPath(kind));
15
- if (!folder) return base;
16
- const resolved = resolve(base, folder);
17
- const rel = relative(base, resolved);
18
- if (rel.startsWith("..") || resolve(base, rel) !== resolved) {
19
- throw new Error(`Folder path escapes vault: "${folder}"`);
20
- }
21
- return resolved;
22
- }
23
-
24
- export function writeEntryFile(
25
- vaultDir,
26
- kind,
27
- {
28
- id,
29
- title,
30
- body,
31
- meta,
32
- tags,
33
- source,
34
- createdAt,
35
- updatedAt,
36
- folder,
37
- category,
38
- identity_key,
39
- expires_at,
40
- supersedes,
41
- related_to,
42
- },
43
- ) {
44
- // P5: folder is now a top-level param; also accept from meta for backward compat
45
- const resolvedFolder = folder || meta?.folder || "";
46
- const dir = safeFolderPath(vaultDir, kind, resolvedFolder);
47
-
48
- try {
49
- mkdirSync(dir, { recursive: true });
50
- } catch (e) {
51
- throw new Error(`Failed to create directory "${dir}": ${e.message}`);
52
- }
53
-
54
- const created = createdAt || new Date().toISOString();
55
- const fmFields = { id };
56
-
57
- // Add kind-specific meta fields to frontmatter (flattened, not nested)
58
- if (meta) {
59
- for (const [k, v] of Object.entries(meta)) {
60
- if (k === "folder") continue;
61
- if (v !== null && v !== undefined) fmFields[k] = v;
62
- }
63
- }
64
-
65
- if (identity_key) fmFields.identity_key = identity_key;
66
- if (expires_at) fmFields.expires_at = expires_at;
67
- if (supersedes?.length) fmFields.supersedes = supersedes;
68
- if (related_to?.length) fmFields.related_to = related_to;
69
- fmFields.tags = tags || [];
70
- fmFields.source = source || "claude-code";
71
- fmFields.created = created;
72
- if (updatedAt && updatedAt !== created) fmFields.updated = updatedAt;
73
-
74
- const mdBody = formatBody(kind, { title, body, meta });
75
-
76
- // Entity kinds: deterministic filename from identity_key (no ULID suffix)
77
- let filename;
78
- if (category === "entity" && identity_key) {
79
- const identitySlug = slugify(identity_key);
80
- filename = identitySlug
81
- ? `${identitySlug}.md`
82
- : `${id.slice(-8).toLowerCase()}.md`;
83
- } else {
84
- const slug = slugify((title || body).slice(0, 40));
85
- const shortId = id.slice(-8).toLowerCase();
86
- filename = slug ? `${slug}-${shortId}.md` : `${shortId}.md`;
87
- }
88
-
89
- const filePath = resolve(dir, filename);
90
- const md = formatFrontmatter(fmFields) + mdBody;
91
-
92
- try {
93
- writeFileSync(filePath, md);
94
- } catch (e) {
95
- throw new Error(`Failed to write entry file "${filePath}": ${e.message}`);
96
- }
97
-
98
- return filePath;
99
- }
@@ -1,46 +0,0 @@
1
- import { captureAndIndex } from "./index.js";
2
-
3
- export async function importEntries(ctx, entries, opts = {}) {
4
- const { onProgress, source } = opts;
5
- let imported = 0;
6
- let failed = 0;
7
- const errors = [];
8
-
9
- for (let i = 0; i < entries.length; i++) {
10
- const entry = entries[i];
11
-
12
- if (onProgress) {
13
- onProgress(i + 1, entries.length);
14
- }
15
-
16
- try {
17
- if (!entry.body?.trim()) {
18
- failed++;
19
- errors.push({ index: i, title: entry.title, error: "Empty body" });
20
- continue;
21
- }
22
-
23
- await captureAndIndex(ctx, {
24
- kind: entry.kind || "insight",
25
- title: entry.title || null,
26
- body: entry.body,
27
- meta: entry.meta,
28
- tags: entry.tags,
29
- source: entry.source || source || "import",
30
- identity_key: entry.identity_key,
31
- expires_at: entry.expires_at,
32
- userId: ctx.userId || null,
33
- });
34
- imported++;
35
- } catch (err) {
36
- failed++;
37
- errors.push({
38
- index: i,
39
- title: entry.title || null,
40
- error: err.message,
41
- });
42
- }
43
- }
44
-
45
- return { imported, failed, errors };
46
- }