@titan-design/brain 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1803 @@
1
+ import {
2
+ createEmbedder
3
+ } from "./chunk-BDNH2E2O.js";
4
+ import {
5
+ addFrontmatterField
6
+ } from "./chunk-4SD4JRLS.js";
7
+
8
+ // src/services/config.ts
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "fs";
10
+ import { join, dirname, resolve } from "path";
11
+ import { homedir } from "os";
12
+ var GLOBAL_BRAIN_DIR = join(homedir(), ".brain");
13
+ function parentResolveOpts(cmd) {
14
+ const parent = cmd.parent?.opts();
15
+ return {
16
+ forceGlobal: parent?.global,
17
+ instancePath: parent?.instance
18
+ };
19
+ }
20
+ var VALID_EMBEDDERS = ["local", "ollama", "remote"];
21
+ function ensureDir(dir) {
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+ function validateConfig(config) {
27
+ if (config.embedder !== void 0 && !VALID_EMBEDDERS.includes(config.embedder)) {
28
+ throw new Error(
29
+ `Invalid embedder "${config.embedder}". Must be one of: ${VALID_EMBEDDERS.join(", ")}`
30
+ );
31
+ }
32
+ if (config.fusionWeights !== void 0) {
33
+ const sum = config.fusionWeights.bm25 + config.fusionWeights.vector;
34
+ if (Math.abs(sum - 1) > 1e-6) {
35
+ throw new Error(
36
+ `Fusion weights must sum to 1.0, got ${sum} (bm25: ${config.fusionWeights.bm25}, vector: ${config.fusionWeights.vector})`
37
+ );
38
+ }
39
+ }
40
+ }
41
+ function resolveInstance(opts = {}) {
42
+ if (opts.instancePath) {
43
+ return { root: resolve(opts.instancePath), isLocal: true, source: "flag:--instance" };
44
+ }
45
+ if (opts.forceGlobal) {
46
+ return { root: GLOBAL_BRAIN_DIR, isLocal: false, source: "flag:--global" };
47
+ }
48
+ let dir = resolve(opts.cwd ?? process.cwd());
49
+ while (true) {
50
+ const candidate = join(dir, ".brain");
51
+ if (existsSync(candidate)) {
52
+ return { root: candidate, isLocal: true, source: `local:${candidate}` };
53
+ }
54
+ const parent = dirname(dir);
55
+ if (parent === dir) break;
56
+ dir = parent;
57
+ }
58
+ return { root: GLOBAL_BRAIN_DIR, isLocal: false, source: "global" };
59
+ }
60
+ function getConfigDir(override) {
61
+ const dir = override ?? GLOBAL_BRAIN_DIR;
62
+ ensureDir(dir);
63
+ return dir;
64
+ }
65
+ function getDataDir(override) {
66
+ const dir = override ?? GLOBAL_BRAIN_DIR;
67
+ ensureDir(dir);
68
+ return dir;
69
+ }
70
+ function getConfigPath(instanceRoot) {
71
+ const root = instanceRoot ?? GLOBAL_BRAIN_DIR;
72
+ ensureDir(root);
73
+ return join(root, "config.json");
74
+ }
75
+ function getDefaultConfig(instanceRoot) {
76
+ const root = instanceRoot ?? GLOBAL_BRAIN_DIR;
77
+ return {
78
+ notesDir: join(homedir(), "brain"),
79
+ dbPath: join(root, "brain.db"),
80
+ embedder: "local",
81
+ fusionWeights: { bm25: 0.3, vector: 0.7 }
82
+ };
83
+ }
84
+ function loadConfig(instance) {
85
+ const globalDefaults = getDefaultConfig(GLOBAL_BRAIN_DIR);
86
+ const globalPath = getConfigPath(GLOBAL_BRAIN_DIR);
87
+ let config = { ...globalDefaults };
88
+ if (existsSync(globalPath)) {
89
+ const raw = JSON.parse(readFileSync(globalPath, "utf-8"));
90
+ config = {
91
+ ...config,
92
+ ...raw,
93
+ fusionWeights: { ...config.fusionWeights, ...raw.fusionWeights }
94
+ };
95
+ }
96
+ if (instance && instance.isLocal) {
97
+ const localDefaults = getDefaultConfig(instance.root);
98
+ config.dbPath = localDefaults.dbPath;
99
+ config.notesDir = join(instance.root, "notes");
100
+ const localPath = getConfigPath(instance.root);
101
+ if (existsSync(localPath)) {
102
+ const raw = JSON.parse(readFileSync(localPath, "utf-8"));
103
+ config = {
104
+ ...config,
105
+ ...raw,
106
+ fusionWeights: { ...config.fusionWeights, ...raw.fusionWeights }
107
+ };
108
+ }
109
+ }
110
+ return config;
111
+ }
112
+ function saveConfig(config, instanceRoot) {
113
+ validateConfig(config);
114
+ const root = instanceRoot ?? GLOBAL_BRAIN_DIR;
115
+ const filePath = getConfigPath(root);
116
+ const defaults = getDefaultConfig(root);
117
+ let existing;
118
+ if (existsSync(filePath)) {
119
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
120
+ existing = {
121
+ ...defaults,
122
+ ...raw,
123
+ fusionWeights: { ...defaults.fusionWeights, ...raw.fusionWeights }
124
+ };
125
+ } else {
126
+ existing = defaults;
127
+ }
128
+ const merged = {
129
+ ...existing,
130
+ ...config,
131
+ fusionWeights: { ...existing.fusionWeights, ...config.fusionWeights }
132
+ };
133
+ writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
134
+ }
135
+
136
+ // src/services/brain-db.ts
137
+ import Database from "better-sqlite3";
138
+ import * as sqliteVec from "sqlite-vec";
139
+ import { mkdirSync as mkdirSync2, renameSync, existsSync as existsSync2 } from "fs";
140
+ import { join as join2, dirname as dirname2, relative } from "path";
141
+
142
+ // src/services/repos/note-repo.ts
143
+ var NoteRepo = class {
144
+ constructor(db, ensureVectorTable) {
145
+ this.db = db;
146
+ this.ensureVectorTable = ensureVectorTable;
147
+ }
148
+ // --- Note CRUD ---
149
+ upsertNote(record) {
150
+ this.db.prepare(
151
+ `INSERT OR REPLACE INTO notes
152
+ (id, file_path, title, type, tier, category, tags, summary, confidence, status, sources, created_at, modified_at, last_reviewed, review_interval, expires, metadata, module, module_instance, content_dir)
153
+ VALUES
154
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
155
+ ).run(
156
+ record.id,
157
+ record.filePath,
158
+ record.title,
159
+ record.type,
160
+ record.tier,
161
+ record.category,
162
+ record.tags,
163
+ record.summary,
164
+ record.confidence,
165
+ record.status,
166
+ record.sources,
167
+ record.createdAt,
168
+ record.modifiedAt,
169
+ record.lastReviewed,
170
+ record.reviewInterval,
171
+ record.expires,
172
+ record.metadata,
173
+ record.module,
174
+ record.moduleInstance,
175
+ record.contentDir
176
+ );
177
+ return record;
178
+ }
179
+ getNoteById(id) {
180
+ const row = this.db.prepare("SELECT * FROM notes WHERE id = ?").get(id);
181
+ return row ? rowToNoteRecord(row) : null;
182
+ }
183
+ getNotesByIds(ids) {
184
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
185
+ const placeholders = ids.map(() => "?").join(",");
186
+ const rows = this.db.prepare(`SELECT * FROM notes WHERE id IN (${placeholders})`).all(...ids);
187
+ const map = /* @__PURE__ */ new Map();
188
+ for (const row of rows) {
189
+ map.set(row.id, rowToNoteRecord(row));
190
+ }
191
+ return map;
192
+ }
193
+ getAllNotes() {
194
+ const rows = this.db.prepare("SELECT * FROM notes").all();
195
+ return rows.map(rowToNoteRecord);
196
+ }
197
+ getNoteCount() {
198
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM notes").get();
199
+ return row.count;
200
+ }
201
+ getNoteByFilePath(filePath) {
202
+ const row = this.db.prepare("SELECT * FROM notes WHERE file_path = ?").get(filePath);
203
+ return row ? rowToNoteRecord(row) : null;
204
+ }
205
+ // --- File Tracking ---
206
+ upsertFile(record) {
207
+ this.db.prepare(
208
+ `INSERT OR REPLACE INTO files (path, hash, mtime, indexed_at)
209
+ VALUES (?, ?, ?, ?)`
210
+ ).run(record.path, record.hash, record.mtime, record.indexedAt);
211
+ }
212
+ getFile(path) {
213
+ const row = this.db.prepare("SELECT * FROM files WHERE path = ?").get(path);
214
+ return row ? rowToFileRecord(row) : null;
215
+ }
216
+ getAllFiles() {
217
+ const rows = this.db.prepare("SELECT * FROM files").all();
218
+ const map = /* @__PURE__ */ new Map();
219
+ for (const row of rows) {
220
+ const rec = rowToFileRecord(row);
221
+ map.set(rec.path, rec);
222
+ }
223
+ return map;
224
+ }
225
+ deleteFile(path) {
226
+ this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
227
+ }
228
+ // --- Chunk + Vector Operations ---
229
+ upsertChunks(noteId, chunks, embeddings) {
230
+ if (chunks.length !== embeddings.length) {
231
+ throw new Error(
232
+ `upsertChunks: chunks (${chunks.length}) and embeddings (${embeddings.length}) length mismatch`
233
+ );
234
+ }
235
+ if (embeddings.length > 0) {
236
+ this.ensureVectorTable(embeddings[0].length);
237
+ }
238
+ this.deleteChunksForNote(noteId);
239
+ const insertChunk = this.db.prepare(
240
+ `INSERT INTO chunks (id, note_id, heading, heading_ancestry, content, token_count, chunk_type, cut_type, position)
241
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
242
+ );
243
+ const insertVector = this.db.prepare(
244
+ `INSERT INTO chunk_vectors (chunk_id, embedding)
245
+ VALUES (?, ?)`
246
+ );
247
+ const txn = this.db.transaction(() => {
248
+ for (let i = 0; i < chunks.length; i++) {
249
+ const chunk = chunks[i];
250
+ insertChunk.run(
251
+ chunk.id,
252
+ noteId,
253
+ chunk.heading,
254
+ chunk.headingAncestry,
255
+ chunk.content,
256
+ chunk.tokenCount,
257
+ chunk.chunkType,
258
+ chunk.cutType,
259
+ chunk.position
260
+ );
261
+ insertVector.run(chunk.id, Buffer.from(embeddings[i].buffer));
262
+ }
263
+ });
264
+ txn();
265
+ }
266
+ getChunksForNote(noteId) {
267
+ const rows = this.db.prepare("SELECT * FROM chunks WHERE note_id = ? ORDER BY rowid").all(noteId);
268
+ return rows.map(rowToChunk);
269
+ }
270
+ getChunkCount() {
271
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM chunks").get();
272
+ return row.count;
273
+ }
274
+ deleteChunksForNote(noteId) {
275
+ const chunkIds = this.db.prepare("SELECT id FROM chunks WHERE note_id = ?").all(noteId);
276
+ if (chunkIds.length > 0) {
277
+ const deleteVec = this.db.prepare("DELETE FROM chunk_vectors WHERE chunk_id = ?");
278
+ const txn = this.db.transaction(() => {
279
+ for (const { id } of chunkIds) {
280
+ deleteVec.run(id);
281
+ }
282
+ });
283
+ txn();
284
+ }
285
+ this.db.prepare("DELETE FROM chunks WHERE note_id = ?").run(noteId);
286
+ }
287
+ getChunkContent(chunkId) {
288
+ const row = this.db.prepare("SELECT content FROM chunks WHERE id = ?").get(chunkId);
289
+ return row?.content ?? "";
290
+ }
291
+ getFirstChunkForNote(noteId) {
292
+ const row = this.db.prepare("SELECT content, heading FROM chunks WHERE note_id = ? ORDER BY rowid LIMIT 1").get(noteId);
293
+ return row ?? null;
294
+ }
295
+ getChunkEmbedding(chunkId) {
296
+ const row = this.db.prepare("SELECT embedding FROM chunk_vectors WHERE chunk_id = ?").get(chunkId);
297
+ if (!row) return null;
298
+ return new Float32Array(
299
+ row.embedding.buffer,
300
+ row.embedding.byteOffset,
301
+ row.embedding.byteLength / 4
302
+ );
303
+ }
304
+ getChunkHeading(chunkId, noteId) {
305
+ if (chunkId) {
306
+ const row2 = this.db.prepare("SELECT heading FROM chunks WHERE id = ?").get(chunkId);
307
+ if (row2?.heading) return row2.heading;
308
+ }
309
+ const row = this.db.prepare("SELECT heading FROM chunks WHERE note_id = ? ORDER BY rowid LIMIT 1").get(noteId);
310
+ return row?.heading ?? null;
311
+ }
312
+ // --- Relations ---
313
+ upsertRelations(noteId, relations) {
314
+ const txn = this.db.transaction(() => {
315
+ this.db.prepare("DELETE FROM relations WHERE source_id = ?").run(noteId);
316
+ const insert = this.db.prepare(
317
+ `INSERT INTO relations (source_id, target_id, type, created_at)
318
+ VALUES (?, ?, ?, ?)`
319
+ );
320
+ for (const rel of relations) {
321
+ insert.run(rel.sourceId, rel.targetId, rel.type, Date.now());
322
+ }
323
+ });
324
+ txn();
325
+ }
326
+ getRelationsFrom(noteId) {
327
+ const rows = this.db.prepare("SELECT source_id, target_id, type FROM relations WHERE source_id = ?").all(noteId);
328
+ return rows.map(rowToRelation);
329
+ }
330
+ getRelationsTo(noteId) {
331
+ const rows = this.db.prepare("SELECT source_id, target_id, type FROM relations WHERE target_id = ?").all(noteId);
332
+ return rows.map(rowToRelation);
333
+ }
334
+ getRelationsBatch(ids) {
335
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
336
+ const result = /* @__PURE__ */ new Map();
337
+ for (const id of ids) {
338
+ result.set(id, { from: [], to: [] });
339
+ }
340
+ const placeholders = ids.map(() => "?").join(",");
341
+ const rows = this.db.prepare(
342
+ `SELECT source_id, target_id, type FROM relations WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`
343
+ ).all(...ids, ...ids);
344
+ for (const row of rows) {
345
+ const rel = rowToRelation(row);
346
+ result.get(rel.sourceId)?.from.push(rel);
347
+ result.get(rel.targetId)?.to.push(rel);
348
+ }
349
+ return result;
350
+ }
351
+ getRelationsFiltered(opts) {
352
+ const conditions = [];
353
+ const params = [];
354
+ if (opts.module) {
355
+ conditions.push("module = ?");
356
+ params.push(opts.module);
357
+ }
358
+ if (opts.moduleInstance) {
359
+ conditions.push("module_instance = ?");
360
+ params.push(opts.moduleInstance);
361
+ }
362
+ if (opts.type) {
363
+ conditions.push("type = ?");
364
+ params.push(opts.type);
365
+ }
366
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
367
+ const rows = this.db.prepare(`SELECT source_id, target_id, type FROM relations ${where}`).all(...params);
368
+ return rows.map(rowToRelation);
369
+ }
370
+ // --- Lineage ---
371
+ getDescendants(noteId, maxDepth) {
372
+ const depthLimit = maxDepth ?? 100;
373
+ const rows = this.db.prepare(
374
+ `
375
+ WITH RECURSIVE descendants(id, depth) AS (
376
+ SELECT source_id, 1 FROM relations
377
+ WHERE target_id = ? AND type = 'derived-from'
378
+ UNION ALL
379
+ SELECT r.source_id, d.depth + 1 FROM relations r
380
+ JOIN descendants d ON r.target_id = d.id
381
+ WHERE r.type = 'derived-from' AND d.depth < ?
382
+ )
383
+ SELECT id, depth FROM descendants
384
+ `
385
+ ).all(noteId, depthLimit);
386
+ return rows;
387
+ }
388
+ // --- Access Tracking ---
389
+ recordAccess(noteId, event) {
390
+ this.db.prepare("INSERT INTO note_access (note_id, event, created_at) VALUES (?, ?, ?)").run(noteId, event, Date.now());
391
+ }
392
+ getAccessCount(noteId) {
393
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM note_access WHERE note_id = ?").get(noteId);
394
+ return row.count;
395
+ }
396
+ getAccessCounts(noteIds) {
397
+ if (noteIds.length === 0) return /* @__PURE__ */ new Map();
398
+ const placeholders = noteIds.map(() => "?").join(",");
399
+ const rows = this.db.prepare(
400
+ `SELECT note_id, COUNT(*) as count FROM note_access
401
+ WHERE note_id IN (${placeholders}) GROUP BY note_id`
402
+ ).all(...noteIds);
403
+ const map = /* @__PURE__ */ new Map();
404
+ for (const row of rows) {
405
+ map.set(row.note_id, row.count);
406
+ }
407
+ return map;
408
+ }
409
+ // --- FTS ---
410
+ upsertNoteFTS(noteId, title, summary, content) {
411
+ this.db.prepare("DELETE FROM notes_fts WHERE note_id = ?").run(noteId);
412
+ this.db.prepare("INSERT INTO notes_fts (note_id, title, summary, content) VALUES (?, ?, ?, ?)").run(noteId, title, summary, content);
413
+ }
414
+ searchFTS(query, limit) {
415
+ if (!query.trim()) return [];
416
+ const sanitized = sanitizeFtsQuery(query);
417
+ if (!sanitized) return [];
418
+ const rows = this.db.prepare(
419
+ `SELECT note_id as noteId, rank
420
+ FROM notes_fts
421
+ WHERE notes_fts MATCH ?
422
+ ORDER BY rank
423
+ LIMIT ?`
424
+ ).all(sanitized, limit);
425
+ return rows;
426
+ }
427
+ // --- Search ---
428
+ searchVector(embedding, limit) {
429
+ try {
430
+ return this.db.prepare(
431
+ `SELECT cv.chunk_id as chunkId, c.note_id as noteId, cv.distance
432
+ FROM chunk_vectors cv
433
+ JOIN chunks c ON c.id = cv.chunk_id
434
+ WHERE embedding MATCH ? AND k = ?
435
+ ORDER BY distance`
436
+ ).all(Buffer.from(embedding.buffer), limit);
437
+ } catch {
438
+ return [];
439
+ }
440
+ }
441
+ getFilteredNoteIds(filters) {
442
+ const conditions = [];
443
+ const params = [];
444
+ if (filters.tier) {
445
+ conditions.push("tier = ?");
446
+ params.push(filters.tier);
447
+ }
448
+ if (filters.category) {
449
+ conditions.push("category = ?");
450
+ params.push(filters.category);
451
+ }
452
+ if (filters.confidence) {
453
+ conditions.push("confidence = ?");
454
+ params.push(filters.confidence);
455
+ }
456
+ if (filters.since) {
457
+ conditions.push("modified_at >= ?");
458
+ params.push(filters.since);
459
+ }
460
+ if (filters.tags?.length) {
461
+ conditions.push(
462
+ `(${filters.tags.map(() => "',' || tags || ',' LIKE '%,' || ? || ',%'").join(" AND ")})`
463
+ );
464
+ for (const tag of filters.tags) params.push(tag);
465
+ }
466
+ if (conditions.length === 0) return null;
467
+ const rows = this.db.prepare(`SELECT id FROM notes WHERE ${conditions.join(" AND ")}`).all(...params);
468
+ return new Set(rows.map((r) => r.id));
469
+ }
470
+ getFilteredNoteIdsByMetadata(filters, baseIds) {
471
+ if (filters.length === 0) return baseIds ?? /* @__PURE__ */ new Set();
472
+ let ids = baseIds ?? null;
473
+ for (const filter of filters) {
474
+ const typeRow = this.db.prepare(
475
+ `SELECT json_type(metadata, '$.' || ?) AS jt FROM notes
476
+ WHERE metadata IS NOT NULL AND json_extract(metadata, '$.' || ?) IS NOT NULL LIMIT 1`
477
+ ).get(filter.field, filter.field);
478
+ const isArray = typeRow?.jt === "array";
479
+ let rows;
480
+ if (isArray) {
481
+ const sql = ids ? `SELECT n.id FROM notes n, json_each(json_extract(n.metadata, '$.' || ?)) AS je
482
+ WHERE CAST(je.value AS TEXT) = ? AND n.id IN (SELECT value FROM json_each(?))` : `SELECT n.id FROM notes n, json_each(json_extract(n.metadata, '$.' || ?)) AS je
483
+ WHERE CAST(je.value AS TEXT) = ?`;
484
+ rows = ids ? this.db.prepare(sql).all(filter.field, filter.value, JSON.stringify([...ids])) : this.db.prepare(sql).all(filter.field, filter.value);
485
+ } else {
486
+ const sql = ids ? `SELECT id FROM notes WHERE json_extract(metadata, '$.' || ?) = ?
487
+ AND id IN (SELECT value FROM json_each(?))` : `SELECT id FROM notes WHERE json_extract(metadata, '$.' || ?) = ?`;
488
+ rows = ids ? this.db.prepare(sql).all(filter.field, filter.value, JSON.stringify([...ids])) : this.db.prepare(sql).all(filter.field, filter.value);
489
+ }
490
+ ids = new Set(rows.map((r) => r.id));
491
+ }
492
+ return ids ?? /* @__PURE__ */ new Set();
493
+ }
494
+ getFacetCounts(field, noteIds) {
495
+ if (noteIds.size === 0) return [];
496
+ const idsJson = JSON.stringify([...noteIds]);
497
+ const typeRow = this.db.prepare(
498
+ `SELECT json_type(metadata, '$.' || ?) AS jt FROM notes
499
+ WHERE metadata IS NOT NULL AND json_extract(metadata, '$.' || ?) IS NOT NULL
500
+ AND id IN (SELECT value FROM json_each(?)) LIMIT 1`
501
+ ).get(field, field, idsJson);
502
+ const isArray = typeRow?.jt === "array";
503
+ if (isArray) {
504
+ return this.db.prepare(
505
+ `SELECT CAST(je.value AS TEXT) AS value, COUNT(DISTINCT n.id) AS count
506
+ FROM notes n, json_each(json_extract(n.metadata, '$.' || ?)) AS je
507
+ WHERE n.id IN (SELECT value FROM json_each(?))
508
+ GROUP BY je.value ORDER BY count DESC, CAST(je.value AS TEXT) ASC`
509
+ ).all(field, idsJson);
510
+ }
511
+ return this.db.prepare(
512
+ `SELECT CAST(json_extract(metadata, '$.' || ?) AS TEXT) AS value, COUNT(*) AS count
513
+ FROM notes
514
+ WHERE id IN (SELECT value FROM json_each(?))
515
+ AND metadata IS NOT NULL AND json_extract(metadata, '$.' || ?) IS NOT NULL
516
+ GROUP BY value ORDER BY count DESC`
517
+ ).all(field, idsJson, field);
518
+ }
519
+ getModuleNoteIds(filter) {
520
+ const conditions = [];
521
+ const params = [];
522
+ if (filter.module) {
523
+ conditions.push("module = ?");
524
+ params.push(filter.module);
525
+ }
526
+ if (filter.moduleInstance) {
527
+ conditions.push("module_instance = ?");
528
+ params.push(filter.moduleInstance);
529
+ }
530
+ if (filter.type) {
531
+ conditions.push("type = ?");
532
+ params.push(filter.type);
533
+ }
534
+ if (filter.status) {
535
+ conditions.push("status = ?");
536
+ params.push(filter.status);
537
+ }
538
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
539
+ const rows = this.db.prepare(`SELECT id FROM notes ${where}`).all(...params);
540
+ return rows.map((r) => r.id);
541
+ }
542
+ };
543
+ function sanitizeFtsQuery(query) {
544
+ return query.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, '""')}"`).join(" ");
545
+ }
546
+ function rowToNoteRecord(row) {
547
+ return {
548
+ id: row.id,
549
+ filePath: row.file_path,
550
+ title: row.title,
551
+ type: row.type,
552
+ tier: row.tier,
553
+ category: row.category,
554
+ tags: row.tags,
555
+ summary: row.summary,
556
+ confidence: row.confidence,
557
+ status: row.status ?? "current",
558
+ sources: row.sources,
559
+ createdAt: row.created_at,
560
+ modifiedAt: row.modified_at,
561
+ lastReviewed: row.last_reviewed,
562
+ reviewInterval: row.review_interval,
563
+ expires: row.expires,
564
+ metadata: row.metadata,
565
+ module: row.module ?? null,
566
+ moduleInstance: row.module_instance ?? null,
567
+ contentDir: row.content_dir ?? null
568
+ };
569
+ }
570
+ function rowToFileRecord(row) {
571
+ return {
572
+ path: row.path,
573
+ hash: row.hash,
574
+ mtime: row.mtime,
575
+ indexedAt: row.indexed_at
576
+ };
577
+ }
578
+ function rowToChunk(row) {
579
+ return {
580
+ id: row.id,
581
+ noteId: row.note_id,
582
+ heading: row.heading,
583
+ headingAncestry: row.heading_ancestry,
584
+ content: row.content,
585
+ tokenCount: row.token_count,
586
+ chunkType: row.chunk_type ?? "section",
587
+ cutType: row.cut_type ?? "heading_boundary",
588
+ position: row.position ?? 0
589
+ };
590
+ }
591
+ function rowToRelation(row) {
592
+ return {
593
+ sourceId: row.source_id,
594
+ targetId: row.target_id,
595
+ type: row.type
596
+ };
597
+ }
598
+
599
+ // src/services/repos/memory-repo.ts
600
+ function rowToMemoryEntry(row) {
601
+ return {
602
+ id: row.id,
603
+ memory: row.memory,
604
+ sourceNoteId: row.source_note_id,
605
+ sourceChunkId: row.source_chunk_id,
606
+ containerTag: row.container_tag,
607
+ isLatest: row.is_latest === 1,
608
+ parentMemoryId: row.parent_memory_id,
609
+ rootMemoryId: row.root_memory_id,
610
+ relationType: row.relation_type,
611
+ validAt: row.valid_at,
612
+ invalidAt: row.invalid_at,
613
+ forgetAfter: row.forget_after,
614
+ isForgotten: row.is_forgotten === 1,
615
+ isInference: row.is_inference === 1,
616
+ createdAt: row.created_at
617
+ };
618
+ }
619
+ function rowToMemoryHistory(row) {
620
+ return {
621
+ id: row.id,
622
+ memoryId: row.memory_id,
623
+ event: row.event,
624
+ oldMemory: row.old_memory,
625
+ newMemory: row.new_memory,
626
+ actor: row.actor,
627
+ createdAt: row.created_at
628
+ };
629
+ }
630
+ var MemoryRepo = class {
631
+ constructor(db) {
632
+ this.db = db;
633
+ }
634
+ queryLatestMemories(baseWhere, baseParams, containerTag) {
635
+ const where = containerTag ? `${baseWhere} AND container_tag = ?` : baseWhere;
636
+ const params = containerTag ? [...baseParams, containerTag] : baseParams;
637
+ const rows = this.db.prepare(`SELECT * FROM memory_entries WHERE ${where} ORDER BY created_at DESC`).all(...params);
638
+ return rows.map(rowToMemoryEntry);
639
+ }
640
+ addMemory(entry) {
641
+ this.db.prepare(
642
+ `INSERT INTO memory_entries
643
+ (id, memory, source_note_id, source_chunk_id, container_tag,
644
+ is_latest, parent_memory_id, root_memory_id, relation_type,
645
+ valid_at, invalid_at, forget_after, is_forgotten, is_inference, created_at)
646
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
647
+ ).run(
648
+ entry.id,
649
+ entry.memory,
650
+ entry.sourceNoteId,
651
+ entry.sourceChunkId,
652
+ entry.containerTag,
653
+ entry.isLatest ? 1 : 0,
654
+ entry.parentMemoryId,
655
+ entry.rootMemoryId,
656
+ entry.relationType,
657
+ entry.validAt,
658
+ entry.invalidAt,
659
+ entry.forgetAfter,
660
+ entry.isForgotten ? 1 : 0,
661
+ entry.isInference ? 1 : 0,
662
+ entry.createdAt
663
+ );
664
+ }
665
+ getMemory(id) {
666
+ const row = this.db.prepare("SELECT * FROM memory_entries WHERE id = ?").get(id);
667
+ return row ? rowToMemoryEntry(row) : null;
668
+ }
669
+ getMemoriesForNote(noteId) {
670
+ const rows = this.db.prepare(
671
+ "SELECT * FROM memory_entries WHERE source_note_id = ? AND is_latest = 1 ORDER BY created_at"
672
+ ).all(noteId);
673
+ return rows.map(rowToMemoryEntry);
674
+ }
675
+ getLatestMemories(containerTag) {
676
+ return this.queryLatestMemories("is_latest = 1 AND is_forgotten = 0", [], containerTag);
677
+ }
678
+ getMemoryVersionChain(rootId) {
679
+ const rows = this.db.prepare(
680
+ "SELECT * FROM memory_entries WHERE root_memory_id = ? OR id = ? ORDER BY created_at"
681
+ ).all(rootId, rootId);
682
+ return rows.map(rowToMemoryEntry);
683
+ }
684
+ markMemorySuperseded(id) {
685
+ this.db.prepare("UPDATE memory_entries SET is_latest = 0 WHERE id = ?").run(id);
686
+ }
687
+ deleteMemoriesForNote(noteId) {
688
+ const memoryIds = this.db.prepare("SELECT id FROM memory_entries WHERE source_note_id = ?").all(noteId);
689
+ if (memoryIds.length > 0) {
690
+ const deleteVector = this.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?");
691
+ const deleteHistory = this.db.prepare("DELETE FROM memory_history WHERE memory_id = ?");
692
+ const txn = this.db.transaction(() => {
693
+ for (const { id } of memoryIds) {
694
+ deleteVector.run(id);
695
+ deleteHistory.run(id);
696
+ }
697
+ });
698
+ txn();
699
+ }
700
+ this.db.prepare("DELETE FROM memory_entries WHERE source_note_id = ?").run(noteId);
701
+ }
702
+ forgetExpiredMemories() {
703
+ const now = (/* @__PURE__ */ new Date()).toISOString();
704
+ const expired = this.db.prepare(
705
+ "SELECT id, memory FROM memory_entries WHERE forget_after IS NOT NULL AND forget_after <= ? AND is_forgotten = 0 AND is_latest = 1"
706
+ ).all(now);
707
+ if (expired.length === 0) return 0;
708
+ const update = this.db.prepare("UPDATE memory_entries SET is_forgotten = 1 WHERE id = ?");
709
+ const insertHistory = this.db.prepare(
710
+ `INSERT INTO memory_history (memory_id, event, old_memory, new_memory, actor, created_at)
711
+ VALUES (?, 'forget', ?, NULL, 'system', ?)`
712
+ );
713
+ const txn = this.db.transaction(() => {
714
+ for (const { id, memory } of expired) {
715
+ update.run(id);
716
+ insertHistory.run(id, memory, now);
717
+ }
718
+ });
719
+ txn();
720
+ return expired.length;
721
+ }
722
+ getMemoriesSince(since, containerTag) {
723
+ return this.queryLatestMemories("created_at >= ? AND is_latest = 1", [since], containerTag);
724
+ }
725
+ getMemoryCount() {
726
+ const row = this.db.prepare(
727
+ "SELECT COUNT(*) as count FROM memory_entries WHERE is_latest = 1 AND is_forgotten = 0"
728
+ ).get();
729
+ return row.count;
730
+ }
731
+ getMemoriesByIds(ids) {
732
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
733
+ const placeholders = ids.map(() => "?").join(",");
734
+ const rows = this.db.prepare(`SELECT * FROM memory_entries WHERE id IN (${placeholders})`).all(...ids);
735
+ const map = /* @__PURE__ */ new Map();
736
+ for (const row of rows) {
737
+ map.set(row.id, rowToMemoryEntry(row));
738
+ }
739
+ return map;
740
+ }
741
+ addMemoryHistory(entry) {
742
+ this.db.prepare(
743
+ `INSERT INTO memory_history (memory_id, event, old_memory, new_memory, actor, created_at)
744
+ VALUES (?, ?, ?, ?, ?, ?)`
745
+ ).run(
746
+ entry.memoryId,
747
+ entry.event,
748
+ entry.oldMemory,
749
+ entry.newMemory,
750
+ entry.actor,
751
+ entry.createdAt
752
+ );
753
+ }
754
+ getMemoryHistory(memoryId) {
755
+ const rows = this.db.prepare("SELECT * FROM memory_history WHERE memory_id = ? ORDER BY created_at").all(memoryId);
756
+ return rows.map(rowToMemoryHistory);
757
+ }
758
+ deleteMemoryVector(memoryId) {
759
+ this.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?").run(memoryId);
760
+ }
761
+ upsertMemoryVector(memoryId, embedding) {
762
+ this.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?").run(memoryId);
763
+ this.db.prepare("INSERT INTO memory_vectors (memory_id, embedding) VALUES (?, ?)").run(memoryId, Buffer.from(embedding.buffer));
764
+ }
765
+ searchMemoryVectors(embedding, limit) {
766
+ try {
767
+ return this.db.prepare(
768
+ `SELECT memory_id as memoryId, distance
769
+ FROM memory_vectors
770
+ WHERE embedding MATCH ? AND k = ?
771
+ ORDER BY distance`
772
+ ).all(Buffer.from(embedding.buffer), limit);
773
+ } catch {
774
+ return [];
775
+ }
776
+ }
777
+ };
778
+
779
+ // src/services/repos/capture-repo.ts
780
+ function rowToInboxItem(row) {
781
+ return {
782
+ id: row.id,
783
+ content: row.content,
784
+ title: row.title,
785
+ source: row.source,
786
+ sourceUrl: row.source_url,
787
+ sourceMeta: row.source_meta,
788
+ status: row.status,
789
+ createdAt: row.created_at,
790
+ processedAt: row.processed_at
791
+ };
792
+ }
793
+ function rowToFeedRecord(row) {
794
+ return {
795
+ id: row.id,
796
+ url: row.url,
797
+ name: row.name,
798
+ containerTag: row.container_tag,
799
+ filterPrompt: row.filter_prompt,
800
+ lastPolled: row.last_polled,
801
+ createdAt: row.created_at
802
+ };
803
+ }
804
+ var CaptureRepo = class {
805
+ constructor(db) {
806
+ this.db = db;
807
+ }
808
+ // --- Inbox ---
809
+ addInboxItem(item) {
810
+ this.db.prepare(
811
+ `INSERT INTO inbox (id, content, title, source, source_url, source_meta, status, created_at, processed_at)
812
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
813
+ ).run(
814
+ item.id,
815
+ item.content,
816
+ item.title,
817
+ item.source,
818
+ item.sourceUrl,
819
+ item.sourceMeta,
820
+ item.status,
821
+ item.createdAt,
822
+ item.processedAt
823
+ );
824
+ }
825
+ getInboxItems(status) {
826
+ if (status) {
827
+ const rows2 = this.db.prepare("SELECT * FROM inbox WHERE status = ? ORDER BY created_at DESC").all(status);
828
+ return rows2.map(rowToInboxItem);
829
+ }
830
+ const rows = this.db.prepare("SELECT * FROM inbox ORDER BY created_at DESC").all();
831
+ return rows.map(rowToInboxItem);
832
+ }
833
+ getInboxItem(id) {
834
+ const row = this.db.prepare("SELECT * FROM inbox WHERE id = ?").get(id);
835
+ return row ? rowToInboxItem(row) : null;
836
+ }
837
+ updateInboxStatus(id, status) {
838
+ const processedAt = status === "indexed" || status === "failed" ? (/* @__PURE__ */ new Date()).toISOString() : null;
839
+ this.db.prepare("UPDATE inbox SET status = ?, processed_at = COALESCE(?, processed_at) WHERE id = ?").run(status, processedAt, id);
840
+ }
841
+ deleteInboxItem(id) {
842
+ this.db.prepare("DELETE FROM inbox WHERE id = ?").run(id);
843
+ }
844
+ // --- Feeds ---
845
+ addFeed(feed) {
846
+ this.db.prepare(
847
+ `INSERT INTO feeds (id, url, name, container_tag, filter_prompt, last_polled, created_at)
848
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
849
+ ).run(
850
+ feed.id,
851
+ feed.url,
852
+ feed.name,
853
+ feed.containerTag,
854
+ feed.filterPrompt,
855
+ feed.lastPolled,
856
+ feed.createdAt
857
+ );
858
+ }
859
+ getFeeds() {
860
+ const rows = this.db.prepare("SELECT * FROM feeds ORDER BY name").all();
861
+ return rows.map(rowToFeedRecord);
862
+ }
863
+ getFeedById(id) {
864
+ const row = this.db.prepare("SELECT * FROM feeds WHERE id = ?").get(id);
865
+ return row ? rowToFeedRecord(row) : null;
866
+ }
867
+ removeFeed(id) {
868
+ this.db.prepare("DELETE FROM feeds WHERE id = ?").run(id);
869
+ }
870
+ updateFeedLastPolled(id, lastPolled) {
871
+ this.db.prepare("UPDATE feeds SET last_polled = ? WHERE id = ?").run(lastPolled, id);
872
+ }
873
+ };
874
+
875
+ // src/services/repos/activity-repo.ts
876
+ function rowToActivityRecord(row) {
877
+ return {
878
+ id: row.id,
879
+ noteIds: row.note_ids,
880
+ module: row.module,
881
+ moduleInstance: row.module_instance,
882
+ activityType: row.activity_type,
883
+ actorType: row.actor_type,
884
+ actorId: row.actor_id,
885
+ sessionId: row.session_id,
886
+ metadata: row.metadata,
887
+ outcome: row.outcome,
888
+ startedAt: row.started_at,
889
+ completedAt: row.completed_at
890
+ };
891
+ }
892
+ var ActivityRepo = class {
893
+ constructor(db) {
894
+ this.db = db;
895
+ }
896
+ addActivity(record) {
897
+ this.db.prepare(
898
+ `INSERT OR REPLACE INTO activities
899
+ (id, note_ids, module, module_instance, activity_type, actor_type, actor_id, session_id, metadata, outcome, started_at, completed_at)
900
+ VALUES
901
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
902
+ ).run(
903
+ record.id,
904
+ record.noteIds,
905
+ record.module,
906
+ record.moduleInstance,
907
+ record.activityType,
908
+ record.actorType,
909
+ record.actorId,
910
+ record.sessionId,
911
+ record.metadata,
912
+ record.outcome,
913
+ record.startedAt,
914
+ record.completedAt
915
+ );
916
+ }
917
+ getActivity(id) {
918
+ const row = this.db.prepare("SELECT * FROM activities WHERE id = ?").get(id);
919
+ return row ? rowToActivityRecord(row) : null;
920
+ }
921
+ getActivities(opts) {
922
+ const conditions = [];
923
+ const params = [];
924
+ if (opts?.module) {
925
+ conditions.push("module = ?");
926
+ params.push(opts.module);
927
+ }
928
+ if (opts?.moduleInstance) {
929
+ conditions.push("module_instance = ?");
930
+ params.push(opts.moduleInstance);
931
+ }
932
+ if (opts?.activityType) {
933
+ conditions.push("activity_type = ?");
934
+ params.push(opts.activityType);
935
+ }
936
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
937
+ const rows = this.db.prepare(`SELECT * FROM activities ${where}`).all(...params);
938
+ return rows.map(rowToActivityRecord);
939
+ }
940
+ getActivitiesByNoteId(noteId) {
941
+ const rows = this.db.prepare(`SELECT * FROM activities WHERE note_ids LIKE '%' || ? || '%'`).all(noteId);
942
+ return rows.map(rowToActivityRecord).filter((a) => {
943
+ if (!a.noteIds) return false;
944
+ try {
945
+ const ids = JSON.parse(a.noteIds);
946
+ return ids.includes(noteId);
947
+ } catch {
948
+ return false;
949
+ }
950
+ });
951
+ }
952
+ getActivitiesBySession(sessionId) {
953
+ const rows = this.db.prepare("SELECT * FROM activities WHERE session_id = ?").all(sessionId);
954
+ return rows.map(rowToActivityRecord);
955
+ }
956
+ };
957
+
958
+ // src/services/brain-db.ts
959
+ var SCHEMA_VERSION = 8;
960
+ var BrainDB = class {
961
+ db;
962
+ vectorDimensions = null;
963
+ noteRepo;
964
+ memoryRepo;
965
+ captureRepo;
966
+ activityRepo;
967
+ constructor(dbPath) {
968
+ this.db = new Database(dbPath);
969
+ this.db.pragma("journal_mode = WAL");
970
+ this.db.pragma("foreign_keys = ON");
971
+ sqliteVec.load(this.db);
972
+ this.migrate();
973
+ this.noteRepo = new NoteRepo(this.db, (dims) => this.ensureVectorTable(dims));
974
+ this.memoryRepo = new MemoryRepo(this.db);
975
+ this.captureRepo = new CaptureRepo(this.db);
976
+ this.activityRepo = new ActivityRepo(this.db);
977
+ }
978
+ close() {
979
+ this.db.close();
980
+ }
981
+ // --- Schema Migration ---
982
+ applyMigration(current, target, fn) {
983
+ if (current >= 1 && current < target) {
984
+ fn();
985
+ this.db.pragma(`user_version = ${target}`);
986
+ this.setMetaValue("schema_version", String(target));
987
+ }
988
+ }
989
+ migrate() {
990
+ const currentVersion = this.db.pragma("user_version", { simple: true });
991
+ if (currentVersion < 1) {
992
+ this.db.exec(this.schemaV1());
993
+ this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
994
+ this.setMetaValue("schema_version", String(SCHEMA_VERSION));
995
+ }
996
+ this.applyMigration(
997
+ currentVersion,
998
+ 2,
999
+ () => this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_note_id ON chunks(note_id)")
1000
+ );
1001
+ this.applyMigration(currentVersion, 3, () => this.migrateToV3());
1002
+ this.applyMigration(currentVersion, 4, () => this.db.exec(this.captureDDL()));
1003
+ this.applyMigration(currentVersion, 5, () => this.db.exec(this.memoryDDL()));
1004
+ this.applyMigration(currentVersion, 6, () => this.db.exec(this.noteAccessDDL()));
1005
+ this.applyMigration(currentVersion, 7, () => this.migrateToV7());
1006
+ this.applyMigration(currentVersion, 8, () => this.migrateToV8());
1007
+ const dims = this.getMetaValue("embedding_dimensions");
1008
+ if (dims) {
1009
+ this.ensureVectorTable(Number(dims));
1010
+ }
1011
+ }
1012
+ ensureVectorTable(dimensions) {
1013
+ if (this.vectorDimensions === dimensions) return;
1014
+ const existing = this.getMetaValue("embedding_dimensions");
1015
+ if (existing && Number(existing) !== dimensions) {
1016
+ this.db.exec("DROP TABLE IF EXISTS chunk_vectors");
1017
+ this.db.exec("DROP TABLE IF EXISTS memory_vectors");
1018
+ }
1019
+ this.db.exec(`
1020
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vectors USING vec0(
1021
+ chunk_id TEXT PRIMARY KEY,
1022
+ embedding float[${dimensions}]
1023
+ )
1024
+ `);
1025
+ this.db.exec(`
1026
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_vectors USING vec0(
1027
+ memory_id TEXT PRIMARY KEY,
1028
+ embedding float[${dimensions}]
1029
+ )
1030
+ `);
1031
+ this.vectorDimensions = dimensions;
1032
+ }
1033
+ captureDDL() {
1034
+ return `
1035
+ CREATE TABLE IF NOT EXISTS inbox (
1036
+ id TEXT PRIMARY KEY,
1037
+ content TEXT NOT NULL,
1038
+ title TEXT,
1039
+ source TEXT NOT NULL DEFAULT 'cli',
1040
+ source_url TEXT,
1041
+ source_meta TEXT,
1042
+ status TEXT NOT NULL DEFAULT 'pending',
1043
+ created_at TEXT NOT NULL,
1044
+ processed_at TEXT
1045
+ );
1046
+
1047
+ CREATE TABLE IF NOT EXISTS feeds (
1048
+ id TEXT PRIMARY KEY,
1049
+ url TEXT NOT NULL UNIQUE,
1050
+ name TEXT NOT NULL,
1051
+ container_tag TEXT NOT NULL DEFAULT 'default',
1052
+ filter_prompt TEXT,
1053
+ last_polled TEXT,
1054
+ created_at TEXT NOT NULL
1055
+ );
1056
+ `;
1057
+ }
1058
+ memoryDDL() {
1059
+ return `
1060
+ CREATE TABLE IF NOT EXISTS memory_entries (
1061
+ id TEXT PRIMARY KEY,
1062
+ memory TEXT NOT NULL,
1063
+ source_note_id TEXT NOT NULL,
1064
+ source_chunk_id TEXT,
1065
+ container_tag TEXT NOT NULL DEFAULT 'default',
1066
+ is_latest INTEGER NOT NULL DEFAULT 1,
1067
+ parent_memory_id TEXT,
1068
+ root_memory_id TEXT,
1069
+ relation_type TEXT,
1070
+ valid_at TEXT,
1071
+ invalid_at TEXT,
1072
+ forget_after TEXT,
1073
+ is_forgotten INTEGER NOT NULL DEFAULT 0,
1074
+ is_inference INTEGER NOT NULL DEFAULT 0,
1075
+ created_at TEXT NOT NULL,
1076
+ FOREIGN KEY (source_note_id) REFERENCES notes(id)
1077
+ );
1078
+
1079
+ CREATE INDEX IF NOT EXISTS idx_memory_source ON memory_entries(source_note_id);
1080
+ CREATE INDEX IF NOT EXISTS idx_memory_latest ON memory_entries(is_latest) WHERE is_latest = 1;
1081
+ CREATE INDEX IF NOT EXISTS idx_memory_container ON memory_entries(container_tag);
1082
+
1083
+ CREATE TABLE IF NOT EXISTS memory_history (
1084
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1085
+ memory_id TEXT NOT NULL,
1086
+ event TEXT NOT NULL,
1087
+ old_memory TEXT,
1088
+ new_memory TEXT,
1089
+ actor TEXT NOT NULL DEFAULT 'system',
1090
+ created_at TEXT NOT NULL,
1091
+ FOREIGN KEY (memory_id) REFERENCES memory_entries(id)
1092
+ );
1093
+
1094
+ CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id);
1095
+ `;
1096
+ }
1097
+ noteAccessDDL() {
1098
+ return `
1099
+ CREATE TABLE IF NOT EXISTS note_access (
1100
+ note_id TEXT NOT NULL,
1101
+ event TEXT NOT NULL,
1102
+ created_at INTEGER NOT NULL
1103
+ );
1104
+ CREATE INDEX IF NOT EXISTS idx_note_access_note ON note_access(note_id);
1105
+ `;
1106
+ }
1107
+ schemaV1() {
1108
+ return `
1109
+ CREATE TABLE IF NOT EXISTS files (
1110
+ path TEXT PRIMARY KEY,
1111
+ hash TEXT NOT NULL,
1112
+ mtime INTEGER NOT NULL,
1113
+ indexed_at INTEGER NOT NULL
1114
+ );
1115
+
1116
+ CREATE TABLE IF NOT EXISTS notes (
1117
+ id TEXT PRIMARY KEY,
1118
+ file_path TEXT NOT NULL UNIQUE,
1119
+ title TEXT NOT NULL,
1120
+ type TEXT NOT NULL,
1121
+ tier TEXT NOT NULL,
1122
+ category TEXT,
1123
+ tags TEXT,
1124
+ summary TEXT,
1125
+ confidence TEXT,
1126
+ status TEXT DEFAULT 'current',
1127
+ sources TEXT,
1128
+ created_at TEXT,
1129
+ modified_at TEXT,
1130
+ last_reviewed TEXT,
1131
+ review_interval TEXT,
1132
+ expires TEXT,
1133
+ metadata TEXT,
1134
+ module TEXT,
1135
+ module_instance TEXT,
1136
+ content_dir TEXT,
1137
+ embed_status TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.embed_status')),
1138
+ activity_type TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.activity_type'))
1139
+ );
1140
+
1141
+ CREATE TABLE IF NOT EXISTS relations (
1142
+ source_id TEXT NOT NULL,
1143
+ target_id TEXT NOT NULL,
1144
+ type TEXT NOT NULL,
1145
+ created_at INTEGER NOT NULL,
1146
+ module TEXT,
1147
+ module_instance TEXT,
1148
+ PRIMARY KEY (source_id, target_id, type)
1149
+ );
1150
+
1151
+ CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
1152
+ note_id UNINDEXED,
1153
+ title,
1154
+ summary,
1155
+ content,
1156
+ tokenize='unicode61'
1157
+ );
1158
+
1159
+ CREATE TABLE IF NOT EXISTS chunks (
1160
+ id TEXT PRIMARY KEY,
1161
+ note_id TEXT NOT NULL,
1162
+ heading TEXT,
1163
+ heading_ancestry TEXT,
1164
+ content TEXT NOT NULL,
1165
+ token_count INTEGER,
1166
+ chunk_type TEXT DEFAULT 'section',
1167
+ cut_type TEXT DEFAULT 'heading_boundary',
1168
+ position INTEGER DEFAULT 0,
1169
+ FOREIGN KEY (note_id) REFERENCES notes(id)
1170
+ );
1171
+
1172
+ CREATE INDEX IF NOT EXISTS idx_chunks_note_id ON chunks(note_id);
1173
+
1174
+ CREATE TABLE IF NOT EXISTS db_meta (
1175
+ key TEXT PRIMARY KEY,
1176
+ value TEXT NOT NULL
1177
+ );
1178
+
1179
+ ${this.captureDDL()}
1180
+ ${this.memoryDDL()}
1181
+ ${this.noteAccessDDL()}
1182
+ ${this.activitiesDDL()}
1183
+ ${this.moduleIndexesDDL()}
1184
+
1185
+ CREATE INDEX IF NOT EXISTS idx_notes_embed_status ON notes(embed_status);
1186
+ CREATE INDEX IF NOT EXISTS idx_notes_activity_type ON notes(activity_type);
1187
+ `;
1188
+ }
1189
+ migrateToV3() {
1190
+ const columns = this.db.pragma("table_info(chunks)");
1191
+ const columnNames = new Set(columns.map((c) => c.name));
1192
+ if (!columnNames.has("heading_ancestry")) {
1193
+ this.db.exec("ALTER TABLE chunks ADD COLUMN heading_ancestry TEXT");
1194
+ }
1195
+ if (!columnNames.has("cut_type")) {
1196
+ this.db.exec("ALTER TABLE chunks ADD COLUMN cut_type TEXT DEFAULT 'heading_boundary'");
1197
+ }
1198
+ if (!columnNames.has("position")) {
1199
+ this.db.exec("ALTER TABLE chunks ADD COLUMN position INTEGER DEFAULT 0");
1200
+ }
1201
+ }
1202
+ migrateToV7() {
1203
+ const noteColumns = this.db.pragma("table_info(notes)");
1204
+ const noteColNames = new Set(noteColumns.map((c) => c.name));
1205
+ if (!noteColNames.has("module")) {
1206
+ this.db.exec("ALTER TABLE notes ADD COLUMN module TEXT");
1207
+ }
1208
+ if (!noteColNames.has("module_instance")) {
1209
+ this.db.exec("ALTER TABLE notes ADD COLUMN module_instance TEXT");
1210
+ }
1211
+ if (!noteColNames.has("content_dir")) {
1212
+ this.db.exec("ALTER TABLE notes ADD COLUMN content_dir TEXT");
1213
+ }
1214
+ const relColumns = this.db.pragma("table_info(relations)");
1215
+ const relColNames = new Set(relColumns.map((c) => c.name));
1216
+ if (!relColNames.has("module")) {
1217
+ this.db.exec("ALTER TABLE relations ADD COLUMN module TEXT");
1218
+ }
1219
+ if (!relColNames.has("module_instance")) {
1220
+ this.db.exec("ALTER TABLE relations ADD COLUMN module_instance TEXT");
1221
+ }
1222
+ this.db.exec(this.activitiesDDL());
1223
+ this.db.exec(this.moduleIndexesDDL());
1224
+ }
1225
+ migrateToV8() {
1226
+ this.db.exec(`
1227
+ ALTER TABLE notes ADD COLUMN embed_status TEXT
1228
+ GENERATED ALWAYS AS (json_extract(metadata, '$.embed_status'));
1229
+ ALTER TABLE notes ADD COLUMN activity_type TEXT
1230
+ GENERATED ALWAYS AS (json_extract(metadata, '$.activity_type'));
1231
+ CREATE INDEX IF NOT EXISTS idx_notes_embed_status ON notes(embed_status);
1232
+ CREATE INDEX IF NOT EXISTS idx_notes_activity_type ON notes(activity_type);
1233
+ `);
1234
+ }
1235
+ activitiesDDL() {
1236
+ return `
1237
+ CREATE TABLE IF NOT EXISTS activities (
1238
+ id TEXT PRIMARY KEY,
1239
+ note_ids TEXT,
1240
+ module TEXT,
1241
+ module_instance TEXT,
1242
+ activity_type TEXT,
1243
+ actor_type TEXT,
1244
+ actor_id TEXT,
1245
+ session_id TEXT,
1246
+ metadata TEXT,
1247
+ outcome TEXT,
1248
+ started_at TEXT,
1249
+ completed_at TEXT
1250
+ );
1251
+ `;
1252
+ }
1253
+ moduleIndexesDDL() {
1254
+ return `
1255
+ CREATE INDEX IF NOT EXISTS idx_notes_module ON notes(module);
1256
+ CREATE INDEX IF NOT EXISTS idx_notes_module_instance ON notes(module, module_instance);
1257
+ CREATE INDEX IF NOT EXISTS idx_activities_module ON activities(module, module_instance);
1258
+ CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(module, activity_type);
1259
+ CREATE INDEX IF NOT EXISTS idx_activities_session ON activities(session_id);
1260
+ `;
1261
+ }
1262
+ // --- Meta ---
1263
+ setMetaValue(key, value) {
1264
+ this.db.prepare("INSERT OR REPLACE INTO db_meta (key, value) VALUES (?, ?)").run(key, value);
1265
+ }
1266
+ getMetaValue(key) {
1267
+ const row = this.db.prepare("SELECT value FROM db_meta WHERE key = ?").get(key);
1268
+ return row?.value ?? null;
1269
+ }
1270
+ // --- Embedding Model ---
1271
+ setEmbeddingModel(model, dimensions) {
1272
+ this.setMetaValue("embedding_model", model);
1273
+ this.setMetaValue("embedding_dimensions", String(dimensions));
1274
+ this.ensureVectorTable(dimensions);
1275
+ }
1276
+ getEmbeddingModel() {
1277
+ const model = this.getMetaValue("embedding_model");
1278
+ const dims = this.getMetaValue("embedding_dimensions");
1279
+ if (!model || !dims) return null;
1280
+ return { model, dimensions: Number(dims) };
1281
+ }
1282
+ checkModelMatch(model) {
1283
+ const stored = this.getEmbeddingModel();
1284
+ if (!stored) return;
1285
+ if (stored.model !== model) {
1286
+ throw new Error(
1287
+ `Embedding model mismatch: DB uses "${stored.model}" but "${model}" was requested. Re-index with --force to switch models.`
1288
+ );
1289
+ }
1290
+ }
1291
+ // --- Table Introspection ---
1292
+ listTables() {
1293
+ const rows = this.db.prepare(
1294
+ "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'"
1295
+ ).all();
1296
+ return rows.map((r) => r.name);
1297
+ }
1298
+ // --- Cross-repo Orchestration ---
1299
+ deleteNote(id) {
1300
+ const txn = this.db.transaction(() => {
1301
+ this.memoryRepo.deleteMemoriesForNote(id);
1302
+ this.noteRepo.deleteChunksForNote(id);
1303
+ this.db.prepare("DELETE FROM notes_fts WHERE note_id = ?").run(id);
1304
+ this.db.prepare("DELETE FROM relations WHERE source_id = ? OR target_id = ?").run(id, id);
1305
+ this.db.prepare("DELETE FROM note_access WHERE note_id = ?").run(id);
1306
+ this.db.prepare("DELETE FROM notes WHERE id = ?").run(id);
1307
+ });
1308
+ txn();
1309
+ }
1310
+ cascadeDeletePreview(noteId) {
1311
+ const descendants = this.noteRepo.getDescendants(noteId);
1312
+ const allIds = [noteId, ...descendants.map((d) => d.id)];
1313
+ let memoryCount = 0;
1314
+ for (const id of allIds) {
1315
+ memoryCount += this.memoryRepo.getMemoriesForNote(id).length;
1316
+ }
1317
+ return { noteIds: allIds, noteCount: allIds.length, memoryCount };
1318
+ }
1319
+ cascadeDelete(noteId) {
1320
+ const preview = this.cascadeDeletePreview(noteId);
1321
+ const txn = this.db.transaction(() => {
1322
+ for (const id of preview.noteIds.filter((id2) => id2 !== noteId).reverse()) {
1323
+ this.deleteNote(id);
1324
+ }
1325
+ this.deleteNote(noteId);
1326
+ });
1327
+ txn();
1328
+ return preview;
1329
+ }
1330
+ cascadeArchive(noteId, notesDir) {
1331
+ const note = this.getNoteById(noteId);
1332
+ if (!note) throw new Error(`Note not found: ${noteId}`);
1333
+ const archiveDir = join2(notesDir, ".archive");
1334
+ const relativePath = relative(notesDir, note.filePath);
1335
+ const archivePath = join2(archiveDir, relativePath);
1336
+ mkdirSync2(dirname2(archivePath), { recursive: true });
1337
+ if (existsSync2(note.filePath)) {
1338
+ renameSync(note.filePath, archivePath);
1339
+ }
1340
+ const txn = this.db.transaction(() => {
1341
+ this.memoryRepo.deleteMemoriesForNote(noteId);
1342
+ this.noteRepo.deleteChunksForNote(noteId);
1343
+ this.db.prepare("DELETE FROM notes_fts WHERE note_id = ?").run(noteId);
1344
+ this.db.prepare("UPDATE notes SET status = ?, file_path = ? WHERE id = ?").run("archived", archivePath, noteId);
1345
+ });
1346
+ txn();
1347
+ const directChildren = this.noteRepo.getDescendants(noteId, 1);
1348
+ const orphanedIds = [];
1349
+ for (const child of directChildren) {
1350
+ const childNote = this.getNoteById(child.id);
1351
+ if (childNote && existsSync2(childNote.filePath)) {
1352
+ addFrontmatterField(childNote.filePath, "orphaned_from", noteId);
1353
+ orphanedIds.push(child.id);
1354
+ }
1355
+ }
1356
+ return { archivedNote: noteId, archivedPath: archivePath, orphanedChildren: orphanedIds };
1357
+ }
1358
+ // --- Note Delegates ---
1359
+ upsertNote(record) {
1360
+ return this.noteRepo.upsertNote(record);
1361
+ }
1362
+ getNoteById(id) {
1363
+ return this.noteRepo.getNoteById(id);
1364
+ }
1365
+ getNotesByIds(ids) {
1366
+ return this.noteRepo.getNotesByIds(ids);
1367
+ }
1368
+ getAllNotes() {
1369
+ return this.noteRepo.getAllNotes();
1370
+ }
1371
+ getNoteCount() {
1372
+ return this.noteRepo.getNoteCount();
1373
+ }
1374
+ getNoteByFilePath(filePath) {
1375
+ return this.noteRepo.getNoteByFilePath(filePath);
1376
+ }
1377
+ // --- File Delegates ---
1378
+ upsertFile(record) {
1379
+ this.noteRepo.upsertFile(record);
1380
+ }
1381
+ getFile(path) {
1382
+ return this.noteRepo.getFile(path);
1383
+ }
1384
+ getAllFiles() {
1385
+ return this.noteRepo.getAllFiles();
1386
+ }
1387
+ deleteFile(path) {
1388
+ this.noteRepo.deleteFile(path);
1389
+ }
1390
+ // --- Chunk Delegates ---
1391
+ upsertChunks(noteId, chunks, embeddings) {
1392
+ this.noteRepo.upsertChunks(noteId, chunks, embeddings);
1393
+ }
1394
+ getChunksForNote(noteId) {
1395
+ return this.noteRepo.getChunksForNote(noteId);
1396
+ }
1397
+ getChunkCount() {
1398
+ return this.noteRepo.getChunkCount();
1399
+ }
1400
+ deleteChunksForNote(noteId) {
1401
+ this.noteRepo.deleteChunksForNote(noteId);
1402
+ }
1403
+ getChunkContent(chunkId) {
1404
+ return this.noteRepo.getChunkContent(chunkId);
1405
+ }
1406
+ getFirstChunkForNote(noteId) {
1407
+ return this.noteRepo.getFirstChunkForNote(noteId);
1408
+ }
1409
+ getChunkHeading(chunkId, noteId) {
1410
+ return this.noteRepo.getChunkHeading(chunkId, noteId);
1411
+ }
1412
+ getChunkEmbedding(chunkId) {
1413
+ return this.noteRepo.getChunkEmbedding(chunkId);
1414
+ }
1415
+ // --- Relation Delegates ---
1416
+ upsertRelations(noteId, relations) {
1417
+ this.noteRepo.upsertRelations(noteId, relations);
1418
+ }
1419
+ getRelationsFrom(noteId) {
1420
+ return this.noteRepo.getRelationsFrom(noteId);
1421
+ }
1422
+ getRelationsTo(noteId) {
1423
+ return this.noteRepo.getRelationsTo(noteId);
1424
+ }
1425
+ getRelationsBatch(ids) {
1426
+ return this.noteRepo.getRelationsBatch(ids);
1427
+ }
1428
+ getDescendants(noteId, maxDepth) {
1429
+ return this.noteRepo.getDescendants(noteId, maxDepth);
1430
+ }
1431
+ getRelationsFiltered(opts) {
1432
+ return this.noteRepo.getRelationsFiltered(opts);
1433
+ }
1434
+ // --- Access Delegates ---
1435
+ recordAccess(noteId, event) {
1436
+ this.noteRepo.recordAccess(noteId, event);
1437
+ }
1438
+ getAccessCount(noteId) {
1439
+ return this.noteRepo.getAccessCount(noteId);
1440
+ }
1441
+ getAccessCounts(noteIds) {
1442
+ return this.noteRepo.getAccessCounts(noteIds);
1443
+ }
1444
+ // --- FTS Delegates ---
1445
+ upsertNoteFTS(noteId, title, summary, content) {
1446
+ this.noteRepo.upsertNoteFTS(noteId, title, summary, content);
1447
+ }
1448
+ searchFTS(query, limit) {
1449
+ return this.noteRepo.searchFTS(query, limit);
1450
+ }
1451
+ // --- Search Delegates ---
1452
+ searchVector(embedding, limit) {
1453
+ return this.noteRepo.searchVector(embedding, limit);
1454
+ }
1455
+ getFilteredNoteIds(filters) {
1456
+ return this.noteRepo.getFilteredNoteIds(filters);
1457
+ }
1458
+ getFilteredNoteIdsByMetadata(filters, baseIds) {
1459
+ return this.noteRepo.getFilteredNoteIdsByMetadata(filters, baseIds);
1460
+ }
1461
+ getFacetCounts(field, noteIds) {
1462
+ return this.noteRepo.getFacetCounts(field, noteIds);
1463
+ }
1464
+ getModuleNoteIds(filter) {
1465
+ return this.noteRepo.getModuleNoteIds(filter);
1466
+ }
1467
+ // --- Memory Delegates ---
1468
+ addMemory(entry) {
1469
+ this.memoryRepo.addMemory(entry);
1470
+ }
1471
+ getMemory(id) {
1472
+ return this.memoryRepo.getMemory(id);
1473
+ }
1474
+ getMemoriesForNote(noteId) {
1475
+ return this.memoryRepo.getMemoriesForNote(noteId);
1476
+ }
1477
+ getLatestMemories(containerTag) {
1478
+ return this.memoryRepo.getLatestMemories(containerTag);
1479
+ }
1480
+ getMemoryVersionChain(rootId) {
1481
+ return this.memoryRepo.getMemoryVersionChain(rootId);
1482
+ }
1483
+ markMemorySuperseded(id) {
1484
+ this.memoryRepo.markMemorySuperseded(id);
1485
+ }
1486
+ deleteMemoriesForNote(noteId) {
1487
+ this.memoryRepo.deleteMemoriesForNote(noteId);
1488
+ }
1489
+ forgetExpiredMemories() {
1490
+ return this.memoryRepo.forgetExpiredMemories();
1491
+ }
1492
+ getMemoriesSince(since, containerTag) {
1493
+ return this.memoryRepo.getMemoriesSince(since, containerTag);
1494
+ }
1495
+ getMemoryCount() {
1496
+ return this.memoryRepo.getMemoryCount();
1497
+ }
1498
+ getMemoriesByIds(ids) {
1499
+ return this.memoryRepo.getMemoriesByIds(ids);
1500
+ }
1501
+ addMemoryHistory(entry) {
1502
+ this.memoryRepo.addMemoryHistory(entry);
1503
+ }
1504
+ getMemoryHistory(memoryId) {
1505
+ return this.memoryRepo.getMemoryHistory(memoryId);
1506
+ }
1507
+ deleteMemoryVector(memoryId) {
1508
+ this.memoryRepo.deleteMemoryVector(memoryId);
1509
+ }
1510
+ upsertMemoryVector(memoryId, embedding) {
1511
+ this.memoryRepo.upsertMemoryVector(memoryId, embedding);
1512
+ }
1513
+ searchMemoryVectors(embedding, limit) {
1514
+ return this.memoryRepo.searchMemoryVectors(embedding, limit);
1515
+ }
1516
+ // --- Capture Delegates ---
1517
+ addInboxItem(item) {
1518
+ this.captureRepo.addInboxItem(item);
1519
+ }
1520
+ getInboxItems(status) {
1521
+ return this.captureRepo.getInboxItems(status);
1522
+ }
1523
+ getInboxItem(id) {
1524
+ return this.captureRepo.getInboxItem(id);
1525
+ }
1526
+ updateInboxStatus(id, status) {
1527
+ this.captureRepo.updateInboxStatus(id, status);
1528
+ }
1529
+ deleteInboxItem(id) {
1530
+ this.captureRepo.deleteInboxItem(id);
1531
+ }
1532
+ addFeed(feed) {
1533
+ this.captureRepo.addFeed(feed);
1534
+ }
1535
+ getFeeds() {
1536
+ return this.captureRepo.getFeeds();
1537
+ }
1538
+ getFeedById(id) {
1539
+ return this.captureRepo.getFeedById(id);
1540
+ }
1541
+ removeFeed(id) {
1542
+ this.captureRepo.removeFeed(id);
1543
+ }
1544
+ updateFeedLastPolled(id, lastPolled) {
1545
+ this.captureRepo.updateFeedLastPolled(id, lastPolled);
1546
+ }
1547
+ // --- Activity Delegates ---
1548
+ addActivity(record) {
1549
+ this.activityRepo.addActivity(record);
1550
+ }
1551
+ getActivity(id) {
1552
+ return this.activityRepo.getActivity(id);
1553
+ }
1554
+ getActivities(opts) {
1555
+ return this.activityRepo.getActivities(opts);
1556
+ }
1557
+ getActivitiesByNoteId(noteId) {
1558
+ return this.activityRepo.getActivitiesByNoteId(noteId);
1559
+ }
1560
+ getActivitiesBySession(sessionId) {
1561
+ return this.activityRepo.getActivitiesBySession(sessionId);
1562
+ }
1563
+ };
1564
+
1565
+ // src/modules/loader.ts
1566
+ import { readdirSync, existsSync as existsSync3 } from "fs";
1567
+ import { join as join3, basename, dirname as dirname3 } from "path";
1568
+ import { fileURLToPath } from "url";
1569
+
1570
+ // src/modules/registry.ts
1571
+ var ModuleRegistry = class {
1572
+ modules = /* @__PURE__ */ new Map();
1573
+ noteTypes = /* @__PURE__ */ new Map();
1574
+ relationTypes = /* @__PURE__ */ new Map();
1575
+ commands = [];
1576
+ extractionStrategies = [];
1577
+ filters = [];
1578
+ migrations = [];
1579
+ contentHandlers = [];
1580
+ registerModule(mod) {
1581
+ if (this.modules.has(mod.name)) {
1582
+ throw new Error(`Module "${mod.name}" is already registered`);
1583
+ }
1584
+ this.modules.set(mod.name, mod);
1585
+ }
1586
+ getModule(name) {
1587
+ return this.modules.get(name);
1588
+ }
1589
+ getModuleNames() {
1590
+ return [...this.modules.keys()];
1591
+ }
1592
+ // --- Note Types ---
1593
+ registerNoteType(moduleName, noteType) {
1594
+ const key = noteType.name;
1595
+ if (this.noteTypes.has(key)) {
1596
+ const existing = this.noteTypes.get(key);
1597
+ throw new Error(`Note type "${key}" already registered by module "${existing.module}"`);
1598
+ }
1599
+ this.noteTypes.set(key, { module: moduleName, noteType });
1600
+ }
1601
+ getNoteType(name) {
1602
+ return this.noteTypes.get(name)?.noteType;
1603
+ }
1604
+ getNoteTypeModule(name) {
1605
+ return this.noteTypes.get(name)?.module;
1606
+ }
1607
+ getAllNoteTypes() {
1608
+ return [...this.noteTypes.values()];
1609
+ }
1610
+ // --- Relation Types ---
1611
+ registerRelationType(moduleName, relationType) {
1612
+ const key = relationType.name;
1613
+ if (this.relationTypes.has(key)) {
1614
+ const existing = this.relationTypes.get(key);
1615
+ throw new Error(`Relation type "${key}" already registered by module "${existing.module}"`);
1616
+ }
1617
+ this.relationTypes.set(key, { module: moduleName, relationType });
1618
+ }
1619
+ getRelationType(name) {
1620
+ return this.relationTypes.get(name)?.relationType;
1621
+ }
1622
+ // --- Commands ---
1623
+ registerCommand(moduleName, command) {
1624
+ this.commands.push({ module: moduleName, command });
1625
+ }
1626
+ getCommands() {
1627
+ return [...this.commands];
1628
+ }
1629
+ // --- Directory Schemas ---
1630
+ getDirectorySchema(noteTypeName) {
1631
+ return this.noteTypes.get(noteTypeName)?.noteType.directorySchema;
1632
+ }
1633
+ // --- Extraction Strategies ---
1634
+ registerExtractionStrategy(moduleName, strategy) {
1635
+ this.extractionStrategies.push({ module: moduleName, strategy });
1636
+ }
1637
+ getExtractionStrategy(moduleName) {
1638
+ return this.extractionStrategies.find((s) => s.module === moduleName)?.strategy;
1639
+ }
1640
+ // --- Filters ---
1641
+ registerFilter(moduleName, filter) {
1642
+ this.filters.push({ module: moduleName, filter });
1643
+ }
1644
+ getFilters() {
1645
+ return [...this.filters];
1646
+ }
1647
+ getFilterForModule(moduleName) {
1648
+ return this.filters.find((f) => f.module === moduleName)?.filter;
1649
+ }
1650
+ // --- Migrations ---
1651
+ registerMigration(moduleName, migration) {
1652
+ this.migrations.push({ module: moduleName, migration });
1653
+ }
1654
+ getMigrations(moduleName) {
1655
+ if (moduleName) {
1656
+ return this.migrations.filter((m) => m.module === moduleName);
1657
+ }
1658
+ return [...this.migrations];
1659
+ }
1660
+ // --- Content Handlers ---
1661
+ registerContentHandler(moduleName, handler) {
1662
+ this.contentHandlers.push({ module: moduleName, handler });
1663
+ }
1664
+ getContentHandlers() {
1665
+ return [...this.contentHandlers];
1666
+ }
1667
+ };
1668
+
1669
+ // src/modules/context.ts
1670
+ function createModuleContext(registry, moduleName) {
1671
+ return {
1672
+ registerNoteType(noteType) {
1673
+ registry.registerNoteType(moduleName, noteType);
1674
+ },
1675
+ registerRelationType(relationType) {
1676
+ registry.registerRelationType(moduleName, relationType);
1677
+ },
1678
+ registerCommand(command) {
1679
+ registry.registerCommand(moduleName, command);
1680
+ },
1681
+ registerExtractionStrategy(strategy) {
1682
+ registry.registerExtractionStrategy(moduleName, strategy);
1683
+ },
1684
+ registerFilter(filter) {
1685
+ registry.registerFilter(moduleName, filter);
1686
+ },
1687
+ registerMigration(migration) {
1688
+ registry.registerMigration(moduleName, migration);
1689
+ },
1690
+ registerContentHandler(handler) {
1691
+ registry.registerContentHandler(moduleName, handler);
1692
+ }
1693
+ };
1694
+ }
1695
+
1696
+ // src/modules/loader.ts
1697
+ function getBuiltinModulesDir() {
1698
+ try {
1699
+ const thisDir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
1700
+ return join3(thisDir, "..", "..", "modules");
1701
+ } catch {
1702
+ return "";
1703
+ }
1704
+ }
1705
+ function discoverModules(modulesDir) {
1706
+ const dir = modulesDir ?? getBuiltinModulesDir();
1707
+ if (!dir || !existsSync3(dir)) return [];
1708
+ return readdirSync(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).filter((f) => !f.startsWith("_") && !f.endsWith(".test.ts") && !f.endsWith(".test.js")).map((f) => join3(dir, f));
1709
+ }
1710
+ async function loadModules(opts) {
1711
+ const registry = new ModuleRegistry();
1712
+ const errors = [];
1713
+ if (opts?.modules) {
1714
+ for (const mod of opts.modules) {
1715
+ try {
1716
+ registry.registerModule(mod);
1717
+ const ctx = createModuleContext(registry, mod.name);
1718
+ mod.register(ctx);
1719
+ } catch (err) {
1720
+ errors.push({
1721
+ module: mod.name,
1722
+ error: err instanceof Error ? err : new Error(String(err))
1723
+ });
1724
+ }
1725
+ }
1726
+ }
1727
+ const modulePaths = discoverModules(opts?.modulesDir);
1728
+ for (const modulePath of modulePaths) {
1729
+ const name = basename(modulePath).replace(/\.[jt]s$/, "");
1730
+ try {
1731
+ const imported = await import(modulePath);
1732
+ const mod = imported.default ?? imported;
1733
+ if (!mod.name || !mod.register) {
1734
+ throw new Error(`Module at ${modulePath} does not export a valid BrainModule`);
1735
+ }
1736
+ registry.registerModule(mod);
1737
+ const ctx = createModuleContext(registry, mod.name);
1738
+ mod.register(ctx);
1739
+ } catch (err) {
1740
+ errors.push({
1741
+ module: name,
1742
+ error: err instanceof Error ? err : new Error(String(err))
1743
+ });
1744
+ }
1745
+ }
1746
+ return { registry, errors };
1747
+ }
1748
+
1749
+ // src/services/brain-service.ts
1750
+ async function withBrain(fn, resolveOpts) {
1751
+ const instance = resolveInstance(resolveOpts);
1752
+ const config = loadConfig(instance);
1753
+ const db = new BrainDB(config.dbPath);
1754
+ const embedder = createEmbedder(config);
1755
+ const { registry } = await loadModules();
1756
+ const svc = {
1757
+ db,
1758
+ embedder,
1759
+ config,
1760
+ modules: registry,
1761
+ instance,
1762
+ close() {
1763
+ db.close();
1764
+ }
1765
+ };
1766
+ try {
1767
+ return await fn(svc);
1768
+ } finally {
1769
+ svc.close();
1770
+ }
1771
+ }
1772
+ async function withDb(fn, resolveOpts) {
1773
+ const instance = resolveInstance(resolveOpts);
1774
+ const config = loadConfig(instance);
1775
+ const db = new BrainDB(config.dbPath);
1776
+ const svc = {
1777
+ db,
1778
+ config,
1779
+ instance,
1780
+ close() {
1781
+ db.close();
1782
+ }
1783
+ };
1784
+ try {
1785
+ return await fn(svc);
1786
+ } finally {
1787
+ svc.close();
1788
+ }
1789
+ }
1790
+
1791
+ export {
1792
+ GLOBAL_BRAIN_DIR,
1793
+ parentResolveOpts,
1794
+ resolveInstance,
1795
+ getConfigDir,
1796
+ getDataDir,
1797
+ loadConfig,
1798
+ saveConfig,
1799
+ BrainDB,
1800
+ loadModules,
1801
+ withBrain,
1802
+ withDb
1803
+ };