brainbank 0.7.0 → 0.8.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.
Files changed (156) hide show
  1. package/README.md +76 -1398
  2. package/bin/brainbank +5 -1
  3. package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
  4. package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
  5. package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
  6. package/dist/chunk-3JZIM5AU.js.map +1 -0
  7. package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
  8. package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
  9. package/dist/chunk-7JDCHUJV.js +89 -0
  10. package/dist/chunk-7JDCHUJV.js.map +1 -0
  11. package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
  12. package/dist/chunk-7T2ZCZQA.js.map +1 -0
  13. package/dist/chunk-E3J37GDA.js +74 -0
  14. package/dist/chunk-E3J37GDA.js.map +1 -0
  15. package/dist/chunk-JEFWMS5Z.js +217 -0
  16. package/dist/chunk-JEFWMS5Z.js.map +1 -0
  17. package/dist/chunk-JRVYSTMP.js +3256 -0
  18. package/dist/chunk-JRVYSTMP.js.map +1 -0
  19. package/dist/chunk-OLDHLOMT.js +69 -0
  20. package/dist/chunk-OLDHLOMT.js.map +1 -0
  21. package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
  22. package/dist/chunk-QTZNB6AK.js.map +1 -0
  23. package/dist/chunk-RFF7HMP6.js +109 -0
  24. package/dist/chunk-RFF7HMP6.js.map +1 -0
  25. package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
  26. package/dist/chunk-TD3TEFI3.js.map +1 -0
  27. package/dist/cli.js +1036 -341
  28. package/dist/cli.js.map +1 -1
  29. package/dist/haiku-expander-WOVJIVXD.js +8 -0
  30. package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
  31. package/dist/http-server-GIRELCCL.js +9 -0
  32. package/dist/index.d.ts +1774 -611
  33. package/dist/index.js +282 -70
  34. package/dist/index.js.map +1 -1
  35. package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
  36. package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
  37. package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
  38. package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
  39. package/dist/plugin-FF4Q34TI.js +32 -0
  40. package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
  41. package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
  42. package/package.json +25 -52
  43. package/src/brainbank.ts +620 -0
  44. package/src/cli/commands/collection.ts +77 -0
  45. package/src/cli/commands/context.ts +171 -0
  46. package/src/cli/commands/daemon.ts +100 -0
  47. package/src/cli/commands/docs.ts +71 -0
  48. package/src/cli/commands/files.ts +69 -0
  49. package/src/cli/commands/help.ts +72 -0
  50. package/src/cli/commands/index.ts +282 -0
  51. package/src/cli/commands/kv.ts +140 -0
  52. package/src/cli/commands/mcp.ts +13 -0
  53. package/src/cli/commands/reembed.ts +30 -0
  54. package/src/cli/commands/scan.ts +365 -0
  55. package/src/cli/commands/search.ts +130 -0
  56. package/src/cli/commands/stats.ts +44 -0
  57. package/src/cli/commands/status.ts +47 -0
  58. package/src/cli/commands/watch.ts +43 -0
  59. package/src/cli/factory/brain-context.ts +43 -0
  60. package/src/cli/factory/builtin-registration.ts +123 -0
  61. package/src/cli/factory/config-loader.ts +72 -0
  62. package/src/cli/factory/index.ts +65 -0
  63. package/src/cli/factory/plugin-loader.ts +146 -0
  64. package/src/cli/index.ts +63 -0
  65. package/src/cli/server-client.ts +135 -0
  66. package/src/cli/utils.ts +121 -0
  67. package/src/config.ts +50 -0
  68. package/src/constants.ts +13 -0
  69. package/src/db/adapter.ts +112 -0
  70. package/src/db/metadata.ts +130 -0
  71. package/src/db/migrations.ts +66 -0
  72. package/src/db/sqlite-adapter.ts +208 -0
  73. package/src/db/tracker.ts +91 -0
  74. package/src/engine/index-api.ts +85 -0
  75. package/src/engine/reembed.ts +206 -0
  76. package/src/engine/search-api.ts +222 -0
  77. package/src/index.ts +159 -0
  78. package/src/lib/fts.ts +57 -0
  79. package/src/lib/languages.ts +180 -0
  80. package/src/lib/logger.ts +125 -0
  81. package/src/lib/math.ts +87 -0
  82. package/src/lib/provider-key.ts +20 -0
  83. package/src/lib/prune.ts +71 -0
  84. package/src/lib/rerank.ts +33 -0
  85. package/src/lib/rrf.ts +133 -0
  86. package/src/lib/write-lock.ts +108 -0
  87. package/src/plugin.ts +323 -0
  88. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  89. package/src/providers/embeddings/embedding-worker.ts +141 -0
  90. package/src/providers/embeddings/local-embedding.ts +115 -0
  91. package/src/providers/embeddings/openai-embedding.ts +167 -0
  92. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  93. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  94. package/src/providers/embeddings/resolve.ts +34 -0
  95. package/src/providers/pruners/haiku-expander.ts +152 -0
  96. package/src/providers/pruners/haiku-pruner.ts +112 -0
  97. package/src/providers/rerankers/qwen3-reranker.ts +180 -0
  98. package/src/providers/vector/hnsw-index.ts +174 -0
  99. package/src/providers/vector/hnsw-loader.ts +129 -0
  100. package/src/search/bm25-boost.ts +61 -0
  101. package/src/search/context-builder.ts +298 -0
  102. package/src/search/keyword/composite-bm25-search.ts +62 -0
  103. package/src/search/types.ts +35 -0
  104. package/src/search/vector/composite-vector-search.ts +76 -0
  105. package/src/search/vector/mmr.ts +64 -0
  106. package/src/services/collection.ts +405 -0
  107. package/src/services/daemon.ts +87 -0
  108. package/src/services/http-server.ts +288 -0
  109. package/src/services/kv-service.ts +65 -0
  110. package/src/services/plugin-registry.ts +109 -0
  111. package/src/services/watch.ts +348 -0
  112. package/src/services/webhook-server.ts +100 -0
  113. package/src/types.ts +504 -0
  114. package/dist/base-3SNc_CeY.d.ts +0 -593
  115. package/dist/chunk-424UFCY7.js.map +0 -1
  116. package/dist/chunk-7EZR47JV.js +0 -232
  117. package/dist/chunk-7EZR47JV.js.map +0 -1
  118. package/dist/chunk-B77KABWH.js.map +0 -1
  119. package/dist/chunk-CCXVL56V.js.map +0 -1
  120. package/dist/chunk-DI3H6JVZ.js +0 -2432
  121. package/dist/chunk-DI3H6JVZ.js.map +0 -1
  122. package/dist/chunk-FGL32LUJ.js +0 -754
  123. package/dist/chunk-FGL32LUJ.js.map +0 -1
  124. package/dist/chunk-JRSKWF6K.js +0 -313
  125. package/dist/chunk-JRSKWF6K.js.map +0 -1
  126. package/dist/chunk-U2Q2XGPZ.js +0 -42
  127. package/dist/chunk-U2Q2XGPZ.js.map +0 -1
  128. package/dist/chunk-VQ27YUHH.js +0 -629
  129. package/dist/chunk-VQ27YUHH.js.map +0 -1
  130. package/dist/chunk-VVXYZIIB.js +0 -304
  131. package/dist/chunk-VVXYZIIB.js.map +0 -1
  132. package/dist/chunk-YOLKSYWK.js +0 -79
  133. package/dist/chunk-YOLKSYWK.js.map +0 -1
  134. package/dist/chunk-ZNLN2VWV.js.map +0 -1
  135. package/dist/code.d.ts +0 -33
  136. package/dist/code.js +0 -9
  137. package/dist/docs.d.ts +0 -21
  138. package/dist/docs.js +0 -9
  139. package/dist/git.d.ts +0 -33
  140. package/dist/git.js +0 -9
  141. package/dist/memory.d.ts +0 -17
  142. package/dist/memory.js +0 -9
  143. package/dist/notes.d.ts +0 -17
  144. package/dist/notes.js +0 -10
  145. package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
  146. package/dist/resolve-CUJWY6HP.js.map +0 -1
  147. /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
  148. /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
  149. /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
  150. /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
  151. /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
  152. /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
  153. /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
  154. /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
  155. /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
  156. /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
@@ -0,0 +1,3256 @@
1
+ import {
2
+ isBM25SearchPlugin,
3
+ isContextFieldPlugin,
4
+ isContextFormatterPlugin,
5
+ isDocsPlugin,
6
+ isExpandablePlugin,
7
+ isFileResolvable,
8
+ isIndexable,
9
+ isReembeddable,
10
+ isSearchable,
11
+ isVectorSearchPlugin,
12
+ isWatchable
13
+ } from "./chunk-E3J37GDA.js";
14
+ import {
15
+ providerKey,
16
+ resolveEmbedding
17
+ } from "./chunk-7T2ZCZQA.js";
18
+ import {
19
+ __name
20
+ } from "./chunk-7QVYU63E.js";
21
+
22
+ // src/config.ts
23
+ import * as path from "path";
24
+ var DEFAULTS = {
25
+ repoPath: ".",
26
+ dbPath: ".brainbank/data/brainbank.db",
27
+ gitDepth: 500,
28
+ maxFileSize: 512e3,
29
+ // 500KB
30
+ maxDiffBytes: 8192,
31
+ hnswM: 16,
32
+ hnswEfConstruction: 200,
33
+ hnswEfSearch: 50,
34
+ embeddingDims: 384,
35
+ maxElements: 2e6
36
+ };
37
+ function resolveConfig(partial = {}) {
38
+ const repoPath = path.resolve(partial.repoPath ?? DEFAULTS.repoPath);
39
+ const rawDbPath = partial.dbPath ?? DEFAULTS.dbPath;
40
+ const dbPath = path.isAbsolute(rawDbPath) ? rawDbPath : path.join(repoPath, rawDbPath);
41
+ return {
42
+ repoPath,
43
+ dbPath,
44
+ gitDepth: partial.gitDepth ?? DEFAULTS.gitDepth,
45
+ maxFileSize: partial.maxFileSize ?? DEFAULTS.maxFileSize,
46
+ maxDiffBytes: partial.maxDiffBytes ?? DEFAULTS.maxDiffBytes,
47
+ hnswM: partial.hnswM ?? DEFAULTS.hnswM,
48
+ hnswEfConstruction: partial.hnswEfConstruction ?? DEFAULTS.hnswEfConstruction,
49
+ hnswEfSearch: partial.hnswEfSearch ?? DEFAULTS.hnswEfSearch,
50
+ embeddingDims: partial.embeddingDims ?? DEFAULTS.embeddingDims,
51
+ maxElements: partial.maxElements ?? DEFAULTS.maxElements,
52
+ embeddingProvider: partial.embeddingProvider,
53
+ reranker: partial.reranker,
54
+ pruner: partial.pruner,
55
+ expander: partial.expander,
56
+ webhookPort: partial.webhookPort,
57
+ contextFields: partial.contextFields
58
+ };
59
+ }
60
+ __name(resolveConfig, "resolveConfig");
61
+
62
+ // src/constants.ts
63
+ var HNSW = {
64
+ KV: "kv"
65
+ };
66
+
67
+ // src/db/sqlite-adapter.ts
68
+ import BetterSqlite3 from "better-sqlite3";
69
+ import * as fs from "fs";
70
+ import * as path2 from "path";
71
+ var SCHEMA_VERSION = 9;
72
+ function createSchema(adapter) {
73
+ adapter.exec(`
74
+ -- \u2500\u2500 Schema versioning \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
75
+ CREATE TABLE IF NOT EXISTS schema_version (
76
+ version INTEGER PRIMARY KEY,
77
+ applied_at INTEGER NOT NULL DEFAULT (unixepoch())
78
+ );
79
+ INSERT OR IGNORE INTO schema_version (version) VALUES (${SCHEMA_VERSION});
80
+
81
+ -- \u2500\u2500 Plugin Versions (migration tracking) \u2500\u2500\u2500\u2500\u2500\u2500
82
+ CREATE TABLE IF NOT EXISTS plugin_versions (
83
+ plugin_name TEXT PRIMARY KEY,
84
+ version INTEGER NOT NULL,
85
+ applied_at INTEGER NOT NULL DEFAULT (unixepoch())
86
+ );
87
+
88
+ -- \u2500\u2500 Dynamic Collections (KV Store) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
89
+ CREATE TABLE IF NOT EXISTS kv_data (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ collection TEXT NOT NULL,
92
+ content TEXT NOT NULL,
93
+ meta_json TEXT NOT NULL DEFAULT '{}',
94
+ tags_json TEXT NOT NULL DEFAULT '[]',
95
+ expires_at INTEGER,
96
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS kv_vectors (
100
+ data_id INTEGER PRIMARY KEY REFERENCES kv_data(id) ON DELETE CASCADE,
101
+ embedding BLOB NOT NULL
102
+ );
103
+
104
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_kv USING fts5(
105
+ content,
106
+ collection,
107
+ content='kv_data',
108
+ content_rowid='id',
109
+ tokenize='porter unicode61'
110
+ );
111
+
112
+ CREATE TRIGGER IF NOT EXISTS trg_fts_kv_insert AFTER INSERT ON kv_data BEGIN
113
+ INSERT INTO fts_kv(rowid, content, collection)
114
+ VALUES (new.id, new.content, new.collection);
115
+ END;
116
+ CREATE TRIGGER IF NOT EXISTS trg_fts_kv_delete AFTER DELETE ON kv_data BEGIN
117
+ INSERT INTO fts_kv(fts_kv, rowid, content, collection)
118
+ VALUES ('delete', old.id, old.content, old.collection);
119
+ END;
120
+
121
+ CREATE INDEX IF NOT EXISTS idx_kv_collection ON kv_data(collection);
122
+ CREATE INDEX IF NOT EXISTS idx_kv_created ON kv_data(created_at DESC);
123
+
124
+ -- \u2500\u2500 Embedding Metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
125
+ CREATE TABLE IF NOT EXISTS embedding_meta (
126
+ key TEXT PRIMARY KEY,
127
+ value TEXT NOT NULL
128
+ );
129
+
130
+ -- \u2500\u2500 Index State (cross-process coordination) \u2500
131
+ CREATE TABLE IF NOT EXISTS index_state (
132
+ name TEXT PRIMARY KEY,
133
+ version INTEGER NOT NULL DEFAULT 0,
134
+ writer_pid INTEGER NOT NULL DEFAULT 0,
135
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
136
+ );
137
+
138
+ -- \u2500\u2500 Plugin Tracking (incremental indexing) \u2500\u2500\u2500\u2500
139
+ CREATE TABLE IF NOT EXISTS plugin_tracking (
140
+ plugin TEXT NOT NULL,
141
+ key TEXT NOT NULL,
142
+ content_hash TEXT NOT NULL,
143
+ indexed_at INTEGER NOT NULL DEFAULT (unixepoch()),
144
+ PRIMARY KEY (plugin, key)
145
+ );
146
+ `);
147
+ }
148
+ __name(createSchema, "createSchema");
149
+ function wrapStatement(stmt) {
150
+ return {
151
+ get(...params) {
152
+ return stmt.get(...params);
153
+ },
154
+ all(...params) {
155
+ return stmt.all(...params);
156
+ },
157
+ run(...params) {
158
+ const info = stmt.run(...params);
159
+ return {
160
+ lastInsertRowid: info.lastInsertRowid,
161
+ changes: info.changes
162
+ };
163
+ },
164
+ iterate(...params) {
165
+ return stmt.iterate(...params);
166
+ }
167
+ };
168
+ }
169
+ __name(wrapStatement, "wrapStatement");
170
+ var SQLiteAdapter = class {
171
+ static {
172
+ __name(this, "SQLiteAdapter");
173
+ }
174
+ _db;
175
+ capabilities = {
176
+ fts: "fts5",
177
+ upsert: "or-replace",
178
+ json: true,
179
+ vectors: false
180
+ };
181
+ constructor(dbPath) {
182
+ const dir = path2.dirname(dbPath);
183
+ if (!fs.existsSync(dir)) {
184
+ fs.mkdirSync(dir, { recursive: true });
185
+ }
186
+ this._db = new BetterSqlite3(dbPath);
187
+ this._db.pragma("journal_mode = WAL");
188
+ this._db.pragma("busy_timeout = 5000");
189
+ this._db.pragma("synchronous = NORMAL");
190
+ this._db.pragma("foreign_keys = ON");
191
+ createSchema(this);
192
+ }
193
+ /** Prepare a reusable statement. */
194
+ prepare(sql) {
195
+ return wrapStatement(this._db.prepare(sql));
196
+ }
197
+ /** Execute raw SQL (no results). */
198
+ exec(sql) {
199
+ this._db.exec(sql);
200
+ }
201
+ /** Run a function inside a transaction. Auto-commits on success, auto-rollbacks on error. */
202
+ transaction(fn) {
203
+ const tx = this._db.transaction(fn);
204
+ return tx();
205
+ }
206
+ /** Run a prepared statement on multiple rows. Wraps in a single transaction. */
207
+ batch(sql, rows) {
208
+ const stmt = this._db.prepare(sql);
209
+ const tx = this._db.transaction(() => {
210
+ for (const row of rows) {
211
+ stmt.run(...row);
212
+ }
213
+ });
214
+ tx();
215
+ }
216
+ /** Close the database. */
217
+ close() {
218
+ this._db.close();
219
+ }
220
+ /**
221
+ * Access the underlying `better-sqlite3` Database instance.
222
+ *
223
+ * @deprecated Use `DatabaseAdapter` methods instead. This exists
224
+ * only for gradual migration of plugins that depend on driver internals.
225
+ */
226
+ raw() {
227
+ return this._db;
228
+ }
229
+ };
230
+
231
+ // src/db/tracker.ts
232
+ function createTracker(db, pluginName) {
233
+ return {
234
+ isUnchanged(key, contentHash) {
235
+ const row = db.prepare(
236
+ "SELECT content_hash FROM plugin_tracking WHERE plugin = ? AND key = ?"
237
+ ).get(pluginName, key);
238
+ return row?.content_hash === contentHash;
239
+ },
240
+ markIndexed(key, contentHash) {
241
+ db.prepare(`
242
+ INSERT INTO plugin_tracking (plugin, key, content_hash)
243
+ VALUES (?, ?, ?)
244
+ ON CONFLICT(plugin, key) DO UPDATE SET
245
+ content_hash = excluded.content_hash,
246
+ indexed_at = unixepoch()
247
+ `).run(pluginName, key, contentHash);
248
+ },
249
+ findOrphans(currentKeys) {
250
+ const rows = db.prepare(
251
+ "SELECT key FROM plugin_tracking WHERE plugin = ?"
252
+ ).all(pluginName);
253
+ return rows.filter((r) => !currentKeys.has(r.key)).map((r) => r.key);
254
+ },
255
+ remove(key) {
256
+ db.prepare(
257
+ "DELETE FROM plugin_tracking WHERE plugin = ? AND key = ?"
258
+ ).run(pluginName, key);
259
+ },
260
+ clear() {
261
+ db.prepare(
262
+ "DELETE FROM plugin_tracking WHERE plugin = ?"
263
+ ).run(pluginName);
264
+ }
265
+ };
266
+ }
267
+ __name(createTracker, "createTracker");
268
+
269
+ // src/db/metadata.ts
270
+ function bumpVersion(db, name) {
271
+ const row = db.prepare(`
272
+ INSERT INTO index_state (name, version, writer_pid, updated_at)
273
+ VALUES (?, 1, ?, unixepoch())
274
+ ON CONFLICT(name) DO UPDATE SET
275
+ version = version + 1,
276
+ writer_pid = excluded.writer_pid,
277
+ updated_at = excluded.updated_at
278
+ RETURNING version
279
+ `).get(name, process.pid);
280
+ return row.version;
281
+ }
282
+ __name(bumpVersion, "bumpVersion");
283
+ function getVersions(db) {
284
+ const rows = db.prepare("SELECT name, version FROM index_state").all();
285
+ const map = /* @__PURE__ */ new Map();
286
+ for (const row of rows) {
287
+ map.set(row.name, row.version);
288
+ }
289
+ return map;
290
+ }
291
+ __name(getVersions, "getVersions");
292
+ function getVersion(db, name) {
293
+ const row = db.prepare("SELECT version FROM index_state WHERE name = ?").get(name);
294
+ return row?.version ?? 0;
295
+ }
296
+ __name(getVersion, "getVersion");
297
+ function getEmbeddingMeta(db) {
298
+ try {
299
+ const provider = db.prepare(
300
+ "SELECT value FROM embedding_meta WHERE key = 'provider'"
301
+ ).get();
302
+ const dims = db.prepare(
303
+ "SELECT value FROM embedding_meta WHERE key = 'dims'"
304
+ ).get();
305
+ const key = db.prepare(
306
+ "SELECT value FROM embedding_meta WHERE key = 'provider_key'"
307
+ ).get();
308
+ if (!provider || !dims) return null;
309
+ return {
310
+ provider: provider.value,
311
+ dims: Number(dims.value),
312
+ providerKey: key?.value ?? "local"
313
+ };
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+ __name(getEmbeddingMeta, "getEmbeddingMeta");
319
+ function setEmbeddingMeta(db, embedding) {
320
+ const upsert = db.prepare(
321
+ "INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)"
322
+ );
323
+ upsert.run("provider", embedding.constructor?.name ?? "unknown");
324
+ upsert.run("dims", String(embedding.dims));
325
+ upsert.run("provider_key", providerKey(embedding));
326
+ upsert.run("indexed_at", (/* @__PURE__ */ new Date()).toISOString());
327
+ }
328
+ __name(setEmbeddingMeta, "setEmbeddingMeta");
329
+ function detectProviderMismatch(db, embedding) {
330
+ const meta = getEmbeddingMeta(db);
331
+ if (!meta) return null;
332
+ const currentName = embedding.constructor?.name ?? "unknown";
333
+ const mismatch = meta.dims !== embedding.dims || meta.provider !== currentName;
334
+ return {
335
+ mismatch,
336
+ stored: `${meta.provider}/${meta.dims}`,
337
+ current: `${currentName}/${embedding.dims}`
338
+ };
339
+ }
340
+ __name(detectProviderMismatch, "detectProviderMismatch");
341
+
342
+ // src/lib/write-lock.ts
343
+ import { openSync, closeSync, unlinkSync, readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, constants } from "fs";
344
+ import { join as join2 } from "path";
345
+ var MAX_WAIT_MS = 3e4;
346
+ var INITIAL_DELAY_MS = 50;
347
+ function isProcessAlive(pid) {
348
+ if (isNaN(pid)) return false;
349
+ try {
350
+ process.kill(pid, 0);
351
+ return true;
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ __name(isProcessAlive, "isProcessAlive");
357
+ function lockPath(lockDir2, name) {
358
+ return join2(lockDir2, `${name}.lock`);
359
+ }
360
+ __name(lockPath, "lockPath");
361
+ function tryCreateLock(filePath) {
362
+ try {
363
+ const fd = openSync(filePath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
364
+ writeFileSync(fd, String(process.pid));
365
+ closeSync(fd);
366
+ return true;
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ __name(tryCreateLock, "tryCreateLock");
372
+ async function acquireLock(lockDir2, name) {
373
+ if (!existsSync2(lockDir2)) {
374
+ mkdirSync2(lockDir2, { recursive: true });
375
+ }
376
+ const fp = lockPath(lockDir2, name);
377
+ let delay = INITIAL_DELAY_MS;
378
+ let elapsed = 0;
379
+ while (true) {
380
+ if (tryCreateLock(fp)) return;
381
+ try {
382
+ const content = readFileSync(fp, "utf-8").trim();
383
+ const pid = parseInt(content, 10);
384
+ if (isNaN(pid) || !isProcessAlive(pid)) {
385
+ try {
386
+ unlinkSync(fp);
387
+ } catch {
388
+ }
389
+ if (tryCreateLock(fp)) return;
390
+ }
391
+ } catch {
392
+ if (tryCreateLock(fp)) return;
393
+ }
394
+ if (elapsed >= MAX_WAIT_MS) {
395
+ throw new Error(`BrainBank: Could not acquire write lock '${name}' after ${MAX_WAIT_MS}ms. Another process may be indexing.`);
396
+ }
397
+ await new Promise((r) => setTimeout(r, delay));
398
+ elapsed += delay;
399
+ delay = Math.min(delay * 2, 2e3);
400
+ }
401
+ }
402
+ __name(acquireLock, "acquireLock");
403
+ function releaseLock(lockDir2, name) {
404
+ try {
405
+ unlinkSync(lockPath(lockDir2, name));
406
+ } catch {
407
+ }
408
+ }
409
+ __name(releaseLock, "releaseLock");
410
+ async function withLock(lockDir2, name, fn) {
411
+ await acquireLock(lockDir2, name);
412
+ try {
413
+ return await fn();
414
+ } finally {
415
+ releaseLock(lockDir2, name);
416
+ }
417
+ }
418
+ __name(withLock, "withLock");
419
+
420
+ // src/lib/math.ts
421
+ function cosineSimilarity(a, b) {
422
+ if (a.length !== b.length) {
423
+ throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
424
+ }
425
+ if (a.length === 0) return 0;
426
+ let dot = 0;
427
+ for (let i = 0; i < a.length; i++) {
428
+ dot += a[i] * b[i];
429
+ }
430
+ return dot;
431
+ }
432
+ __name(cosineSimilarity, "cosineSimilarity");
433
+ function normalize(vec) {
434
+ let norm = 0;
435
+ for (let i = 0; i < vec.length; i++) {
436
+ norm += vec[i] * vec[i];
437
+ }
438
+ norm = Math.sqrt(norm);
439
+ if (norm === 0) return new Float32Array(vec.length);
440
+ const result = new Float32Array(vec.length);
441
+ for (let i = 0; i < vec.length; i++) {
442
+ result[i] = vec[i] / norm;
443
+ }
444
+ return result;
445
+ }
446
+ __name(normalize, "normalize");
447
+ function vecToBuffer(vec) {
448
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
449
+ }
450
+ __name(vecToBuffer, "vecToBuffer");
451
+
452
+ // src/lib/rerank.ts
453
+ async function rerank(query, results, reranker) {
454
+ const documents = results.map((r) => r.content);
455
+ const scores = await reranker.rank(query, documents);
456
+ const blended = results.map((r, i) => {
457
+ const pos = i + 1;
458
+ const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.6 : 0.4;
459
+ return {
460
+ ...r,
461
+ score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0)
462
+ };
463
+ });
464
+ return blended.sort((a, b) => b.score - a.score);
465
+ }
466
+ __name(rerank, "rerank");
467
+
468
+ // src/lib/rrf.ts
469
+ function reciprocalRankFusion(resultSets, k = 60, maxResults = 15) {
470
+ const fused = /* @__PURE__ */ new Map();
471
+ for (const results of resultSets) {
472
+ for (let rank = 0; rank < results.length; rank++) {
473
+ const r = results[rank];
474
+ const key = resultKey(r);
475
+ const rrfContribution = 1 / (k + rank + 1);
476
+ const existing = fused.get(key);
477
+ if (existing) {
478
+ existing.rrfScore += rrfContribution;
479
+ if (r.score > existing.result.score) {
480
+ existing.result = { ...r };
481
+ }
482
+ } else {
483
+ fused.set(key, {
484
+ result: { ...r },
485
+ rrfScore: rrfContribution
486
+ });
487
+ }
488
+ }
489
+ }
490
+ const sorted = Array.from(fused.values()).sort((a, b) => b.rrfScore - a.rrfScore).slice(0, maxResults);
491
+ const maxRRF = sorted[0]?.rrfScore ?? 1;
492
+ return sorted.map((entry) => ({
493
+ ...entry.result,
494
+ score: entry.rrfScore / maxRRF,
495
+ metadata: {
496
+ ...entry.result.metadata,
497
+ rrfScore: entry.rrfScore
498
+ }
499
+ }));
500
+ }
501
+ __name(reciprocalRankFusion, "reciprocalRankFusion");
502
+ function resultKey(r) {
503
+ switch (r.type) {
504
+ case "code":
505
+ return `code:${r.filePath}:${r.metadata.startLine}-${r.metadata.endLine}`;
506
+ case "commit":
507
+ return `commit:${r.metadata.hash || r.metadata.shortHash}`;
508
+ case "document":
509
+ return `document:${r.filePath ?? ""}:${r.metadata.collection ?? ""}:${r.metadata.seq ?? ""}:${r.content?.slice(0, 80)}`;
510
+ case "collection":
511
+ return `collection:${r.metadata.id ?? r.content?.slice(0, 80)}`;
512
+ }
513
+ }
514
+ __name(resultKey, "resultKey");
515
+ function fuseRankedLists(lists, keyFn, scoreFn, k = 60, maxResults = 15) {
516
+ const fused = /* @__PURE__ */ new Map();
517
+ for (const list of lists) {
518
+ for (let rank = 0; rank < list.length; rank++) {
519
+ const item = list[rank];
520
+ const key = keyFn(item);
521
+ const contribution = 1 / (k + rank + 1);
522
+ const score = scoreFn(item);
523
+ const existing = fused.get(key);
524
+ if (existing) {
525
+ existing.rrfScore += contribution;
526
+ if (score > existing.bestScore) {
527
+ existing.item = item;
528
+ existing.bestScore = score;
529
+ }
530
+ } else {
531
+ fused.set(key, { item, rrfScore: contribution, bestScore: score });
532
+ }
533
+ }
534
+ }
535
+ const sorted = [...fused.values()].sort((a, b) => b.rrfScore - a.rrfScore).slice(0, maxResults);
536
+ const maxRRF = sorted[0]?.rrfScore ?? 1;
537
+ return sorted.map((e) => ({ item: e.item, score: e.rrfScore / maxRRF }));
538
+ }
539
+ __name(fuseRankedLists, "fuseRankedLists");
540
+
541
+ // src/lib/prune.ts
542
+ var MAX_PREVIEW_CHARS = 8e3;
543
+ async function pruneResults(query, results, pruner) {
544
+ if (results.length <= 1) return results;
545
+ const items = results.map((r, i) => ({
546
+ id: i,
547
+ filePath: r.filePath ?? "unknown",
548
+ preview: _buildPreview(r.content),
549
+ metadata: r.metadata
550
+ }));
551
+ const keepIds = await pruner.prune(query, items);
552
+ const validIds = new Set(Array.from({ length: results.length }, (_, i) => i));
553
+ return keepIds.filter((id) => validIds.has(id)).map((id) => results[id]);
554
+ }
555
+ __name(pruneResults, "pruneResults");
556
+ function _buildPreview(content) {
557
+ if (content.length <= MAX_PREVIEW_CHARS) return content;
558
+ const lines = content.split("\n");
559
+ const totalLines = lines.length;
560
+ const topCount = Math.floor(totalLines * 0.6);
561
+ const bottomCount = Math.floor(totalLines * 0.25);
562
+ const omitted = totalLines - topCount - bottomCount;
563
+ const topPart = lines.slice(0, topCount).join("\n");
564
+ const bottomPart = lines.slice(totalLines - bottomCount).join("\n");
565
+ return `${topPart}
566
+
567
+ // [... ${omitted} lines omitted ...]
568
+
569
+ ${bottomPart}`;
570
+ }
571
+ __name(_buildPreview, "_buildPreview");
572
+
573
+ // src/search/bm25-boost.ts
574
+ function filterByPath(results, prefix) {
575
+ if (!prefix) return results;
576
+ return results.filter((r) => r.filePath?.startsWith(prefix));
577
+ }
578
+ __name(filterByPath, "filterByPath");
579
+
580
+ // src/lib/logger.ts
581
+ import * as fs2 from "fs";
582
+ var LOG_PATH = "/tmp/brainbank.log";
583
+ var MAX_BYTES = 10 * 1024 * 1024;
584
+ function logQuery(entry) {
585
+ try {
586
+ _truncateIfNeeded();
587
+ fs2.appendFileSync(LOG_PATH, _formatEntry(entry));
588
+ } catch {
589
+ }
590
+ }
591
+ __name(logQuery, "logQuery");
592
+ function _formatEntry(e) {
593
+ const divider = "\u2550".repeat(70);
594
+ const lines = [
595
+ "",
596
+ divider,
597
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${e.source.toUpperCase()} \xB7 ${e.method}`,
598
+ `Query: "${e.query}"`,
599
+ `Embedding: ${e.embedding} | Pruner: ${e.pruner ?? "none"} | Reranker: ${e.reranker ?? "none"}`
600
+ ];
601
+ const opts = Object.entries(e.options).filter(([, v]) => v !== void 0);
602
+ if (opts.length > 0) {
603
+ const parts = opts.map(
604
+ ([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`
605
+ );
606
+ lines.push(parts.join(" | "));
607
+ }
608
+ lines.push(`Duration: ${e.durationMs}ms`);
609
+ lines.push("");
610
+ const resultCount = e.results.length;
611
+ const prunedCount = e.pruned?.length ?? 0;
612
+ const header = prunedCount > 0 ? `Results (${resultCount + prunedCount} \u2192 ${resultCount} after pruning):` : `Results (${resultCount}):`;
613
+ lines.push(header);
614
+ for (let i = 0; i < e.results.length; i++) {
615
+ const r = e.results[i];
616
+ const pct = Math.round(r.score * 100);
617
+ const name = r.name ? `[${r.name}]` : "";
618
+ lines.push(` #${String(i + 1).padStart(2)} ${String(pct).padStart(3)}% ${r.filePath.padEnd(45)} ${name}`);
619
+ }
620
+ if (e.pruned && e.pruned.length > 0) {
621
+ lines.push("");
622
+ lines.push(`Pruned (${e.pruned.length} removed):`);
623
+ for (const r of e.pruned) {
624
+ const name = r.name ? `[${r.name}]` : "";
625
+ lines.push(` \u2717 ${r.filePath.padEnd(45)} ${name}`);
626
+ }
627
+ }
628
+ lines.push(divider);
629
+ lines.push("");
630
+ return lines.join("\n");
631
+ }
632
+ __name(_formatEntry, "_formatEntry");
633
+ function _truncateIfNeeded() {
634
+ try {
635
+ const stat = fs2.statSync(LOG_PATH);
636
+ if (stat.size < MAX_BYTES) return;
637
+ const content = fs2.readFileSync(LOG_PATH, "utf-8");
638
+ const half = Math.floor(content.length / 2);
639
+ const nextEntry = content.indexOf("\n\u2550", half);
640
+ if (nextEntry > 0) {
641
+ fs2.writeFileSync(LOG_PATH, "[truncated]\n" + content.slice(nextEntry + 1));
642
+ }
643
+ } catch {
644
+ }
645
+ }
646
+ __name(_truncateIfNeeded, "_truncateIfNeeded");
647
+
648
+ // src/search/context-builder.ts
649
+ var ContextBuilder = class {
650
+ constructor(_search, _registry, _pruner, _embedding, _rerankerName, _configFields = {}, _expander) {
651
+ this._search = _search;
652
+ this._registry = _registry;
653
+ this._pruner = _pruner;
654
+ this._embedding = _embedding;
655
+ this._rerankerName = _rerankerName;
656
+ this._configFields = _configFields;
657
+ this._expander = _expander;
658
+ }
659
+ static {
660
+ __name(this, "ContextBuilder");
661
+ }
662
+ /** Set config-level context field defaults (from config.json "context" section). */
663
+ set configFields(fields) {
664
+ this._configFields = fields;
665
+ }
666
+ /** Set the expander instance. */
667
+ set expander(expander) {
668
+ this._expander = expander;
669
+ }
670
+ /** Build a full context block for a task. Returns markdown for system prompt. */
671
+ async build(task, options = {}) {
672
+ const t0 = Date.now();
673
+ const src = options.sources ?? {};
674
+ const { minScore = 0.25, useMMR = true, mmrLambda = 0.7 } = options;
675
+ let results = this._search ? await this._search.search(task, {
676
+ sources: src,
677
+ minScore,
678
+ useMMR,
679
+ mmrLambda
680
+ }) : [];
681
+ results = filterByPath(results, options.pathPrefix);
682
+ const pruner = options.pruner ?? this._pruner;
683
+ const beforePrune = results;
684
+ if (pruner && results.length > 1) {
685
+ results = await pruneResults(task, results, pruner);
686
+ }
687
+ if (options.excludeFiles && options.excludeFiles.size > 0) {
688
+ results = results.filter((r) => !r.filePath || !options.excludeFiles.has(r.filePath));
689
+ }
690
+ const resolvedFields = this._resolveFields(options);
691
+ let expanderNote;
692
+ if (resolvedFields.expander === true && this._expander && results.length > 0) {
693
+ const expansion = await this._expand(task, results);
694
+ if (expansion.results.length > 0) {
695
+ results = [...results, ...expansion.results];
696
+ }
697
+ expanderNote = expansion.note;
698
+ }
699
+ const parts = [`# Context for: "${task}"
700
+ `];
701
+ this._appendFormatterResults(results, parts, options, resolvedFields);
702
+ await this._appendSearchableResults(task, src, minScore, parts);
703
+ if (expanderNote) {
704
+ parts.push(`
705
+ ## Expansion Notes
706
+
707
+ ${expanderNote}
708
+ `);
709
+ }
710
+ const prunedResults = pruner ? beforePrune.filter((r) => !results.includes(r)) : [];
711
+ logQuery({
712
+ source: options.source ?? "api",
713
+ method: "getContext",
714
+ query: task,
715
+ embedding: this._embedding ? providerKey(this._embedding) : "unknown",
716
+ pruner: pruner ? _prunerName(pruner) : null,
717
+ reranker: this._rerankerName ?? null,
718
+ options: {
719
+ sources: src,
720
+ pathPrefix: options.pathPrefix,
721
+ minScore,
722
+ affectedFiles: options.affectedFiles
723
+ },
724
+ results: results.map(_toLogResult),
725
+ pruned: prunedResults.length > 0 ? prunedResults.map(_toLogResult) : void 0,
726
+ durationMs: Date.now() - t0
727
+ });
728
+ return parts.join("\n");
729
+ }
730
+ /** Invoke ContextFormatterPlugins. Multi-repo: each plugin formats its own results. */
731
+ _appendFormatterResults(results, parts, options, resolvedFields) {
732
+ const fields = resolvedFields ?? this._resolveFields(options);
733
+ const seenFormatters = /* @__PURE__ */ new Set();
734
+ for (const mod of this._registry.all) {
735
+ if (!isContextFormatterPlugin(mod)) continue;
736
+ const baseType = mod.name.split(":")[0];
737
+ const colonIdx = mod.name.indexOf(":");
738
+ if (colonIdx > 0) {
739
+ const repoPrefix = mod.name.slice(colonIdx + 1);
740
+ const scoped = results.filter(
741
+ (r) => r.filePath?.startsWith(repoPrefix + "/")
742
+ );
743
+ if (scoped.length > 0) {
744
+ mod.formatContext(scoped, parts, fields);
745
+ }
746
+ continue;
747
+ }
748
+ if (seenFormatters.has(baseType)) continue;
749
+ seenFormatters.add(baseType);
750
+ mod.formatContext(results, parts, fields);
751
+ }
752
+ }
753
+ /**
754
+ * Resolve context fields: plugin defaults ← config.json ← per-query.
755
+ * Returns a flat Record with the final value for each field.
756
+ */
757
+ _resolveFields(options) {
758
+ const defaults = {};
759
+ for (const mod of this._registry.all) {
760
+ if (isContextFieldPlugin(mod)) {
761
+ for (const field of mod.contextFields()) {
762
+ defaults[field.name] = field.default;
763
+ }
764
+ }
765
+ }
766
+ return { ...defaults, ...this._configFields, ...options.fields ?? {} };
767
+ }
768
+ /**
769
+ * Run LLM expansion: build manifest of candidate chunks from files
770
+ * NOT already in search results, call expander, resolve selected IDs.
771
+ */
772
+ async _expand(task, results) {
773
+ if (!this._expander) return { results: [] };
774
+ const repoPrefix = this._detectRepoPrefix(results);
775
+ const excludeFilePaths = [...new Set(
776
+ results.filter((r) => r.filePath).map((r) => {
777
+ const fp = r.filePath;
778
+ return repoPrefix && fp.startsWith(repoPrefix + "/") ? fp.slice(repoPrefix.length + 1) : fp;
779
+ })
780
+ )];
781
+ const excludeIds = [];
782
+ for (const r of results) {
783
+ const meta = r.metadata;
784
+ const id = meta?.id;
785
+ if (id !== void 0) excludeIds.push(id);
786
+ }
787
+ const manifest = [];
788
+ let resolver;
789
+ for (const mod of this._registry.all) {
790
+ if (!isExpandablePlugin(mod)) continue;
791
+ const colonIdx = mod.name.indexOf(":");
792
+ const pluginRepo = colonIdx > 0 ? mod.name.slice(colonIdx + 1) : void 0;
793
+ if (repoPrefix && pluginRepo && pluginRepo !== repoPrefix) continue;
794
+ manifest.push(...mod.buildManifest(excludeFilePaths, excludeIds));
795
+ if (!resolver) {
796
+ const prefix = pluginRepo;
797
+ resolver = /* @__PURE__ */ __name((ids) => {
798
+ const chunks = mod.resolveChunks(ids);
799
+ if (prefix) {
800
+ for (const chunk of chunks) {
801
+ if (chunk.filePath) chunk.filePath = `${prefix}/${chunk.filePath}`;
802
+ const meta = chunk.metadata;
803
+ if (typeof meta.filePath === "string") {
804
+ meta.filePath = `${prefix}/${meta.filePath}`;
805
+ }
806
+ }
807
+ }
808
+ return chunks;
809
+ }, "resolver");
810
+ }
811
+ }
812
+ if (manifest.length === 0 || !resolver) return { results: [] };
813
+ try {
814
+ const expandResult = await this._expander.expand(task, excludeIds, manifest);
815
+ if (expandResult.ids.length === 0) return { results: [], note: expandResult.note };
816
+ return { results: resolver(expandResult.ids), note: expandResult.note };
817
+ } catch {
818
+ return { results: [] };
819
+ }
820
+ }
821
+ /** Detect the multi-repo prefix from existing results (e.g. 'servicehub-frontend'). */
822
+ _detectRepoPrefix(results) {
823
+ for (const r of results) {
824
+ if (!r.filePath) continue;
825
+ const srcIdx = r.filePath.indexOf("/src/");
826
+ if (srcIdx > 0) return r.filePath.slice(0, srcIdx);
827
+ }
828
+ return void 0;
829
+ }
830
+ /** Collect results from SearchablePlugins that don't have their own formatter. */
831
+ async _appendSearchableResults(task, sources, minScore, parts) {
832
+ for (const mod of this._registry.all) {
833
+ if (isContextFormatterPlugin(mod)) continue;
834
+ if (!isSearchable(mod)) continue;
835
+ const hits = await mod.search(task, { k: sources[mod.name.split(":")[0]] ?? 6, minScore });
836
+ if (hits.length > 0) {
837
+ parts.push(`## ${mod.name}
838
+ `);
839
+ for (const r of hits) {
840
+ parts.push(`- [${Math.round(r.score * 100)}%] ${r.content.slice(0, 200)}`);
841
+ }
842
+ parts.push("");
843
+ }
844
+ }
845
+ }
846
+ };
847
+ function _toLogResult(r) {
848
+ const meta = r.metadata;
849
+ return {
850
+ filePath: r.filePath ?? "unknown",
851
+ score: r.score,
852
+ type: r.type,
853
+ name: meta?.name ?? void 0
854
+ };
855
+ }
856
+ __name(_toLogResult, "_toLogResult");
857
+ function _prunerName(pruner) {
858
+ return pruner.constructor?.name ?? "custom";
859
+ }
860
+ __name(_prunerName, "_prunerName");
861
+
862
+ // src/search/keyword/composite-bm25-search.ts
863
+ var DEFAULT_K = 8;
864
+ var CompositeBM25Search = class {
865
+ constructor(_registry) {
866
+ this._registry = _registry;
867
+ }
868
+ static {
869
+ __name(this, "CompositeBM25Search");
870
+ }
871
+ /**
872
+ * Run BM25 keyword search across all plugins that implement BM25SearchPlugin.
873
+ * Each plugin searches its own FTS5 tables.
874
+ */
875
+ async search(query, options = {}) {
876
+ const src = options.sources ?? {};
877
+ const results = [];
878
+ for (const plugin of this._registry.all) {
879
+ if (!isBM25SearchPlugin(plugin)) continue;
880
+ const baseType = plugin.name.split(":")[0];
881
+ const k = src[baseType] ?? DEFAULT_K;
882
+ if (k <= 0) continue;
883
+ const hits = plugin.searchBM25(query, k);
884
+ const colonIdx = plugin.name.indexOf(":");
885
+ if (colonIdx > 0) {
886
+ const subRepo = plugin.name.slice(colonIdx + 1);
887
+ for (const hit of hits) {
888
+ if (hit.filePath) hit.filePath = `${subRepo}/${hit.filePath}`;
889
+ const meta = hit.metadata;
890
+ if (typeof meta.filePath === "string") {
891
+ meta.filePath = `${subRepo}/${meta.filePath}`;
892
+ }
893
+ }
894
+ }
895
+ results.push(...hits);
896
+ }
897
+ return results.sort((a, b) => b.score - a.score);
898
+ }
899
+ /** Rebuild FTS5 indices across all BM25 plugins. */
900
+ rebuild() {
901
+ for (const plugin of this._registry.all) {
902
+ if (!isBM25SearchPlugin(plugin)) continue;
903
+ plugin.rebuildFTS?.();
904
+ }
905
+ }
906
+ };
907
+
908
+ // src/search/vector/composite-vector-search.ts
909
+ var CompositeVectorSearch = class _CompositeVectorSearch {
910
+ constructor(_c) {
911
+ this._c = _c;
912
+ }
913
+ static {
914
+ __name(this, "CompositeVectorSearch");
915
+ }
916
+ /** Default K when no source override is provided. */
917
+ static DEFAULT_K = 6;
918
+ /** Search across all registered domain strategies with score-based merge. */
919
+ async search(query, options = {}) {
920
+ const src = options.sources ?? {};
921
+ const { minScore = 0.25, useMMR = true, mmrLambda = 0.7 } = options;
922
+ const queryVec = await this._c.embedding.embed(query);
923
+ const allResults = [];
924
+ let requestedK = 0;
925
+ for (const [name, strategy] of this._c.strategies) {
926
+ const baseName = name.split(":")[0];
927
+ const k = src[name] ?? src[baseName] ?? this._c.defaults?.[name] ?? _CompositeVectorSearch.DEFAULT_K;
928
+ if (k <= 0) continue;
929
+ requestedK = Math.max(requestedK, k);
930
+ const hits = strategy.search(queryVec, k, minScore, useMMR, mmrLambda, query);
931
+ const colonIdx = name.indexOf(":");
932
+ if (colonIdx > 0) {
933
+ const subRepo = name.slice(colonIdx + 1);
934
+ for (const hit of hits) {
935
+ if (hit.filePath) hit.filePath = `${subRepo}/${hit.filePath}`;
936
+ const meta = hit.metadata;
937
+ if (typeof meta.filePath === "string") {
938
+ meta.filePath = `${subRepo}/${meta.filePath}`;
939
+ }
940
+ }
941
+ }
942
+ allResults.push(...hits);
943
+ }
944
+ if (allResults.length === 0) return [];
945
+ allResults.sort((a, b) => b.score - a.score);
946
+ const capped = allResults.slice(0, requestedK);
947
+ const maxScore = capped[0].score;
948
+ if (maxScore > 0) {
949
+ for (const r of capped) r.score = r.score / maxScore;
950
+ }
951
+ return capped;
952
+ }
953
+ };
954
+
955
+ // src/providers/vector/hnsw-index.ts
956
+ import { existsSync as existsSync3 } from "fs";
957
+ var HNSWIndex = class {
958
+ constructor(_dims, _maxElements = 2e6, _M = 16, _efConstruction = 200, _efSearch = 50) {
959
+ this._dims = _dims;
960
+ this._maxElements = _maxElements;
961
+ this._M = _M;
962
+ this._efConstruction = _efConstruction;
963
+ this._efSearch = _efSearch;
964
+ }
965
+ static {
966
+ __name(this, "HNSWIndex");
967
+ }
968
+ _index = null;
969
+ _lib = null;
970
+ _ids = /* @__PURE__ */ new Set();
971
+ /**
972
+ * Initialize the HNSW index.
973
+ * Must be called before add/search.
974
+ */
975
+ async init() {
976
+ this._lib = await import("hnswlib-node");
977
+ this._createIndex();
978
+ return this;
979
+ }
980
+ /**
981
+ * Reinitialize the index in-place, clearing all vectors.
982
+ * Required after reembed or full re-index to avoid duplicate IDs.
983
+ * init() must have been called first.
984
+ */
985
+ reinit() {
986
+ if (!this._lib) throw new Error("HNSW not initialized \u2014 call init() first");
987
+ this._createIndex();
988
+ }
989
+ _createIndex() {
990
+ if (!this._lib) throw new Error("HNSW lib not loaded");
991
+ const HNSW2 = this._lib.default?.HierarchicalNSW ?? this._lib.HierarchicalNSW;
992
+ if (!HNSW2) throw new Error("HierarchicalNSW not found in hnswlib-node module");
993
+ this._index = new HNSW2("cosine", this._dims);
994
+ this._index.initIndex(this._maxElements, this._M, this._efConstruction);
995
+ this._index.setEf(this._efSearch);
996
+ this._ids = /* @__PURE__ */ new Set();
997
+ }
998
+ /** Maximum capacity of this index. */
999
+ get maxElements() {
1000
+ return this._maxElements;
1001
+ }
1002
+ /**
1003
+ * Add a vector with an integer ID.
1004
+ * The vector should be pre-normalized for cosine distance.
1005
+ */
1006
+ add(vector, id) {
1007
+ if (!this._index) throw new Error("HNSW index not initialized \u2014 call init() first");
1008
+ if (this._ids.has(id)) return;
1009
+ if (this._ids.size >= this._maxElements) {
1010
+ throw new Error(
1011
+ `HNSW index full (${this._maxElements} elements). Increase maxElements in config or prune old data.`
1012
+ );
1013
+ }
1014
+ this._index.addPoint(Array.from(vector), id);
1015
+ this._ids.add(id);
1016
+ }
1017
+ /**
1018
+ * Mark a vector as deleted so it no longer appears in searches.
1019
+ * Uses hnswlib-node markDelete under the hood.
1020
+ * Safe to call with an ID that doesn't exist.
1021
+ */
1022
+ remove(id) {
1023
+ if (!this._index || this._ids.size === 0) return;
1024
+ if (!this._ids.has(id)) return;
1025
+ try {
1026
+ this._index.markDelete(id);
1027
+ this._ids.delete(id);
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Search for the k nearest neighbors.
1033
+ * Returns results sorted by score (highest first).
1034
+ * Score is 1 - cosine_distance (1.0 = identical).
1035
+ */
1036
+ search(query, k) {
1037
+ if (!this._index || this._ids.size === 0) return [];
1038
+ const actualK = Math.min(k, this._ids.size);
1039
+ const result = this._index.searchKnn(Array.from(query), actualK);
1040
+ return result.neighbors.map((id, i) => ({
1041
+ id,
1042
+ score: 1 - result.distances[i]
1043
+ }));
1044
+ }
1045
+ /** Number of vectors in the index. */
1046
+ get size() {
1047
+ return this._ids.size;
1048
+ }
1049
+ /**
1050
+ * Save the HNSW graph to disk.
1051
+ * The file can be loaded later with tryLoad() to skip vector-by-vector insertion.
1052
+ */
1053
+ save(path9) {
1054
+ if (!this._index || this._ids.size === 0) return;
1055
+ this._index.writeIndexSync(path9);
1056
+ }
1057
+ /**
1058
+ * Try to load a previously saved HNSW index from disk.
1059
+ * Returns true if loaded successfully, false if stale or missing.
1060
+ * @param path File path to the saved index
1061
+ * @param expectedCount Expected number of vectors (from SQLite) — used to detect staleness
1062
+ */
1063
+ tryLoad(path9, expectedCount) {
1064
+ if (!this._index || !existsSync3(path9)) return false;
1065
+ try {
1066
+ this._index.readIndexSync(path9);
1067
+ const loadedCount = this._index.getCurrentCount();
1068
+ if (loadedCount !== expectedCount) {
1069
+ this.reinit();
1070
+ return false;
1071
+ }
1072
+ const ids = this._index.getIdsList();
1073
+ this._ids = new Set(ids);
1074
+ this._index.setEf(this._efSearch);
1075
+ return true;
1076
+ } catch {
1077
+ this.reinit();
1078
+ return false;
1079
+ }
1080
+ }
1081
+ };
1082
+
1083
+ // src/lib/fts.ts
1084
+ function splitCompound(word) {
1085
+ return word.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_\-./\\]/g, " ").trim();
1086
+ }
1087
+ __name(splitCompound, "splitCompound");
1088
+ function sanitizeFTS(query) {
1089
+ const clean = query.replace(/[{}[\]()^~*:]/g, " ").replace(/\bAND\b|\bOR\b|\bNOT\b|\bNEAR\b/gi, "").trim();
1090
+ const expanded = clean.split(/\s+/).map((w) => splitCompound(w)).join(" ");
1091
+ const words = expanded.split(/\s+/).filter((w) => w.length > 1);
1092
+ if (words.length === 0) return "";
1093
+ return words.map((w) => `"${w}"`).join(" ");
1094
+ }
1095
+ __name(sanitizeFTS, "sanitizeFTS");
1096
+ function normalizeBM25(rawScore) {
1097
+ const abs = Math.abs(rawScore);
1098
+ return 1 / (1 + Math.exp(-0.3 * (abs - 5)));
1099
+ }
1100
+ __name(normalizeBM25, "normalizeBM25");
1101
+ function escapeLike(s) {
1102
+ return s.replace(/[%_\\]/g, "\\$&");
1103
+ }
1104
+ __name(escapeLike, "escapeLike");
1105
+
1106
+ // src/services/collection.ts
1107
+ var Collection = class {
1108
+ constructor(_name, _db, _embedding, _hnsw, _vecs, _reranker) {
1109
+ this._name = _name;
1110
+ this._db = _db;
1111
+ this._embedding = _embedding;
1112
+ this._hnsw = _hnsw;
1113
+ this._vecs = _vecs;
1114
+ this._reranker = _reranker;
1115
+ }
1116
+ static {
1117
+ __name(this, "Collection");
1118
+ }
1119
+ /** Collection name. */
1120
+ get name() {
1121
+ return this._name;
1122
+ }
1123
+ /** Add an item. Returns its ID. */
1124
+ async add(content, options = {}) {
1125
+ const opts = "tags" in options || "ttl" in options || "metadata" in options ? options : { metadata: options };
1126
+ const metadata = opts.metadata ?? {};
1127
+ const tags = opts.tags ?? [];
1128
+ const expiresAt = opts.ttl ? Math.floor(Date.now() / 1e3) + parseDuration(opts.ttl) : null;
1129
+ const vec = await this._embedding.embed(content);
1130
+ const result = this._db.prepare(
1131
+ "INSERT INTO kv_data (collection, content, meta_json, tags_json, expires_at) VALUES (?, ?, ?, ?, ?)"
1132
+ ).run(this._name, content, JSON.stringify(metadata), JSON.stringify(tags), expiresAt);
1133
+ const id = Number(result.lastInsertRowid);
1134
+ this._db.prepare(
1135
+ "INSERT INTO kv_vectors (data_id, embedding) VALUES (?, ?)"
1136
+ ).run(id, vecToBuffer(vec));
1137
+ this._hnsw.add(vec, id);
1138
+ this._vecs.set(id, vec);
1139
+ return id;
1140
+ }
1141
+ /** Update an item's content (re-embeds). Returns the new ID. */
1142
+ async update(id, content, options) {
1143
+ const row = this._db.prepare(
1144
+ "SELECT * FROM kv_data WHERE id = ? AND collection = ?"
1145
+ ).get(id, this._name);
1146
+ if (!row) throw new Error(`BrainBank: Item ${id} not found in collection '${this._name}'.`);
1147
+ const metadata = options?.metadata ?? JSON.parse(row.meta_json || "{}");
1148
+ const tags = options?.tags ?? JSON.parse(row.tags_json || "[]");
1149
+ const ttl = options?.ttl;
1150
+ this._removeById(id);
1151
+ return this.add(content, { metadata, tags, ...ttl ? { ttl } : {} });
1152
+ }
1153
+ /** Add multiple items. Returns their IDs. */
1154
+ async addMany(items) {
1155
+ if (items.length === 0) return [];
1156
+ const texts = items.map((i) => i.content);
1157
+ const vecs = await this._embedding.embedBatch(texts);
1158
+ const ids = [];
1159
+ const insertData = this._db.prepare(
1160
+ "INSERT INTO kv_data (collection, content, meta_json, tags_json, expires_at) VALUES (?, ?, ?, ?, ?)"
1161
+ );
1162
+ const insertVec = this._db.prepare(
1163
+ "INSERT INTO kv_vectors (data_id, embedding) VALUES (?, ?)"
1164
+ );
1165
+ this._db.transaction(() => {
1166
+ for (let i = 0; i < items.length; i++) {
1167
+ const item = items[i];
1168
+ const expiresAt = item.ttl ? Math.floor(Date.now() / 1e3) + parseDuration(item.ttl) : null;
1169
+ const result = insertData.run(
1170
+ this._name,
1171
+ item.content,
1172
+ JSON.stringify(item.metadata ?? {}),
1173
+ JSON.stringify(item.tags ?? []),
1174
+ expiresAt
1175
+ );
1176
+ const id = Number(result.lastInsertRowid);
1177
+ insertVec.run(id, vecToBuffer(vecs[i]));
1178
+ ids.push(id);
1179
+ }
1180
+ });
1181
+ for (let i = 0; i < ids.length; i++) {
1182
+ this._hnsw.add(vecs[i], ids[i]);
1183
+ this._vecs.set(ids[i], vecs[i]);
1184
+ }
1185
+ return ids;
1186
+ }
1187
+ /** Search this collection. */
1188
+ async search(query, options = {}) {
1189
+ const { k = 5, mode = "hybrid", minScore = 0.15, tags } = options;
1190
+ this._pruneExpired();
1191
+ if (mode === "keyword") return this._filterByTags(this._searchBM25(query, k, minScore), tags);
1192
+ if (mode === "vector") return this._filterByTags(await this._searchVector(query, k, minScore), tags);
1193
+ const [vectorHits, bm25Hits] = await Promise.all([
1194
+ this._searchVector(query, k, 0),
1195
+ Promise.resolve(this._searchBM25(query, k, 0))
1196
+ ]);
1197
+ const fused = fuseRankedLists(
1198
+ [vectorHits, bm25Hits],
1199
+ (h) => String(h.id),
1200
+ (h) => h.score ?? 0
1201
+ );
1202
+ const results = fused.map(({ item, score }) => ({ ...item, score })).filter((r) => r.score >= minScore).slice(0, k);
1203
+ if (this._reranker && results.length > 1) {
1204
+ const asSearchResults = results.map((r) => ({
1205
+ type: "collection",
1206
+ score: r.score ?? 0,
1207
+ content: r.content,
1208
+ metadata: { id: r.id }
1209
+ }));
1210
+ const reranked = await rerank(query, asSearchResults, this._reranker);
1211
+ const rerankedById = new Map(
1212
+ reranked.map((r) => [r.type === "collection" ? r.metadata.id : void 0, r.score])
1213
+ );
1214
+ const blended = results.map((r) => ({ ...r, score: rerankedById.get(r.id) ?? r.score ?? 0 }));
1215
+ return this._filterByTags(
1216
+ blended.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
1217
+ tags
1218
+ );
1219
+ }
1220
+ return this._filterByTags(results, tags);
1221
+ }
1222
+ /** Search and return results as SearchResult[] for use in hybrid search pipelines. */
1223
+ async searchAsResults(query, k) {
1224
+ const hits = await this.search(query, { k });
1225
+ return hits.map((h) => ({
1226
+ type: "collection",
1227
+ score: h.score ?? 0,
1228
+ content: h.content,
1229
+ metadata: { ...h.metadata, id: h.id, collection: this._name }
1230
+ }));
1231
+ }
1232
+ /** List items (newest first). */
1233
+ list(options = {}) {
1234
+ const { limit = 20, offset = 0, tags } = options;
1235
+ this._pruneExpired();
1236
+ const rows = this._db.prepare(
1237
+ "SELECT * FROM kv_data WHERE collection = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?"
1238
+ ).all(this._name, Math.floor(Date.now() / 1e3), limit, offset);
1239
+ return this._filterByTags(rows.map((r) => this._rowToItem(r)), tags);
1240
+ }
1241
+ /** Count items in this collection. */
1242
+ count() {
1243
+ return this._db.prepare(
1244
+ "SELECT COUNT(*) as c FROM kv_data WHERE collection = ? AND (expires_at IS NULL OR expires_at > ?)"
1245
+ ).get(this._name, Math.floor(Date.now() / 1e3)).c;
1246
+ }
1247
+ /** Keep only the N most recent items, remove the rest. */
1248
+ async trim(options) {
1249
+ const before = this.count();
1250
+ if (before <= options.keep) return { removed: 0 };
1251
+ const toRemove = this._db.prepare(`
1252
+ SELECT id FROM kv_data
1253
+ WHERE collection = ?
1254
+ ORDER BY created_at DESC, id DESC
1255
+ LIMIT -1 OFFSET ?
1256
+ `).all(this._name, options.keep);
1257
+ for (const row of toRemove) {
1258
+ this._removeById(row.id);
1259
+ }
1260
+ return { removed: toRemove.length };
1261
+ }
1262
+ /** Remove items older than a duration string (e.g. '30d', '12h'). */
1263
+ async prune(options) {
1264
+ const seconds = parseDuration(options.olderThan);
1265
+ const cutoff = Math.floor(Date.now() / 1e3) - seconds;
1266
+ const toRemove = this._db.prepare(
1267
+ "SELECT id FROM kv_data WHERE collection = ? AND created_at < ?"
1268
+ ).all(this._name, cutoff);
1269
+ for (const row of toRemove) {
1270
+ this._removeById(row.id);
1271
+ }
1272
+ return { removed: toRemove.length };
1273
+ }
1274
+ /** Remove a specific item by ID. */
1275
+ remove(id) {
1276
+ this._removeById(id);
1277
+ }
1278
+ /** Clear all items in this collection. */
1279
+ clear() {
1280
+ const rows = this._db.prepare(
1281
+ "SELECT id FROM kv_data WHERE collection = ?"
1282
+ ).all(this._name);
1283
+ for (const row of rows) {
1284
+ this._removeById(row.id);
1285
+ }
1286
+ }
1287
+ _removeById(id) {
1288
+ this._db.prepare("DELETE FROM kv_data WHERE id = ?").run(id);
1289
+ this._hnsw.remove(id);
1290
+ this._vecs.delete(id);
1291
+ }
1292
+ async _searchVector(query, k, minScore) {
1293
+ if (this._hnsw.size === 0) return [];
1294
+ const queryVec = await this._embedding.embed(query);
1295
+ const searchK = this._adaptiveSearchK(k);
1296
+ const hits = this._hnsw.search(queryVec, searchK);
1297
+ const ids = hits.map((h) => h.id);
1298
+ if (ids.length === 0) return [];
1299
+ const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
1300
+ const placeholders = ids.map(() => "?").join(",");
1301
+ const rows = this._db.prepare(
1302
+ `SELECT * FROM kv_data WHERE id IN (${placeholders}) AND collection = ?`
1303
+ ).all(...ids, this._name);
1304
+ return rows.map((r) => ({ ...this._rowToItem(r), score: scoreMap.get(r.id) ?? 0 })).filter((r) => r.score >= minScore).sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, k);
1305
+ }
1306
+ /** Compute adaptive over-fetch multiplier based on collection density in shared HNSW. */
1307
+ _adaptiveSearchK(k) {
1308
+ const totalSize = this._hnsw.size;
1309
+ if (totalSize === 0) return 0;
1310
+ const collectionCount = this.count();
1311
+ if (collectionCount === 0) return Math.min(k * 3, totalSize);
1312
+ const ratio = Math.ceil(totalSize / collectionCount);
1313
+ const multiplier = Math.max(3, Math.min(ratio, 50));
1314
+ return Math.min(k * multiplier, totalSize);
1315
+ }
1316
+ _searchBM25(query, k, minScore) {
1317
+ const ftsQuery = sanitizeFTS(query);
1318
+ if (!ftsQuery) return [];
1319
+ try {
1320
+ const rows = this._db.prepare(`
1321
+ SELECT d.*, bm25(fts_kv, 5.0, 1.0) AS score
1322
+ FROM fts_kv f
1323
+ JOIN kv_data d ON d.id = f.rowid
1324
+ WHERE fts_kv MATCH ? AND d.collection = ?
1325
+ ORDER BY score ASC
1326
+ LIMIT ?
1327
+ `).all(ftsQuery, this._name, k);
1328
+ return rows.map((r) => ({
1329
+ ...this._rowToItem(r),
1330
+ score: normalizeBM25(r.score)
1331
+ })).filter((r) => (r.score ?? 0) >= minScore);
1332
+ } catch {
1333
+ return [];
1334
+ }
1335
+ }
1336
+ _rowToItem(r) {
1337
+ return {
1338
+ id: r.id,
1339
+ collection: r.collection,
1340
+ content: r.content,
1341
+ metadata: JSON.parse(r.meta_json || "{}"),
1342
+ tags: JSON.parse(r.tags_json || "[]"),
1343
+ createdAt: r.created_at,
1344
+ expiresAt: r.expires_at ?? void 0
1345
+ };
1346
+ }
1347
+ /** Filter results by tags (item must have ALL specified tags). */
1348
+ _filterByTags(items, tags) {
1349
+ if (!tags || tags.length === 0) return items;
1350
+ return items.filter(
1351
+ (item) => tags.every((t) => item.tags.includes(t))
1352
+ );
1353
+ }
1354
+ /** Remove expired items (TTL). Called automatically on search/list. */
1355
+ _pruneExpired() {
1356
+ const now = Math.floor(Date.now() / 1e3);
1357
+ const expired = this._db.prepare(
1358
+ "SELECT id FROM kv_data WHERE collection = ? AND expires_at IS NOT NULL AND expires_at <= ?"
1359
+ ).all(this._name, now);
1360
+ for (const row of expired) {
1361
+ this._removeById(row.id);
1362
+ }
1363
+ }
1364
+ };
1365
+ function parseDuration(s) {
1366
+ const match = s.match(/^(\d+)([dhms])$/);
1367
+ if (!match) throw new Error(`Invalid duration: "${s}". Use format like '30d', '12h', '5m'.`);
1368
+ const n = parseInt(match[1], 10);
1369
+ switch (match[2]) {
1370
+ case "d":
1371
+ return n * 86400;
1372
+ case "h":
1373
+ return n * 3600;
1374
+ case "m":
1375
+ return n * 60;
1376
+ case "s":
1377
+ return n;
1378
+ default:
1379
+ return n;
1380
+ }
1381
+ }
1382
+ __name(parseDuration, "parseDuration");
1383
+
1384
+ // src/services/kv-service.ts
1385
+ var KVService = class {
1386
+ constructor(_db, _embedding, _hnsw, _vecs, _reranker) {
1387
+ this._db = _db;
1388
+ this._embedding = _embedding;
1389
+ this._hnsw = _hnsw;
1390
+ this._vecs = _vecs;
1391
+ this._reranker = _reranker;
1392
+ }
1393
+ static {
1394
+ __name(this, "KVService");
1395
+ }
1396
+ _collections = /* @__PURE__ */ new Map();
1397
+ /** Get or create a named collection. */
1398
+ collection(name) {
1399
+ if (this._collections.has(name)) return this._collections.get(name);
1400
+ const coll = new Collection(name, this._db, this._embedding, this._hnsw, this._vecs, this._reranker);
1401
+ this._collections.set(name, coll);
1402
+ return coll;
1403
+ }
1404
+ /** List all collection names that have data. */
1405
+ listNames() {
1406
+ return this._db.prepare("SELECT DISTINCT collection FROM kv_data ORDER BY collection").all().map((r) => r.collection);
1407
+ }
1408
+ /** Delete a collection's data and evict from cache. Removes vectors from HNSW to prevent ghost entries. */
1409
+ delete(name) {
1410
+ const ids = this._db.prepare(
1411
+ "SELECT id FROM kv_data WHERE collection = ?"
1412
+ ).all(name);
1413
+ for (const { id } of ids) {
1414
+ this._hnsw.remove(id);
1415
+ this._vecs.delete(id);
1416
+ }
1417
+ this._db.prepare("DELETE FROM kv_data WHERE collection = ?").run(name);
1418
+ this._collections.delete(name);
1419
+ }
1420
+ /** Access the shared HNSW index (used by reembed). */
1421
+ get hnsw() {
1422
+ return this._hnsw;
1423
+ }
1424
+ /** Access the shared vector cache. @internal */
1425
+ get vecs() {
1426
+ return this._vecs;
1427
+ }
1428
+ /** Clear all cached collections and vectors. */
1429
+ clear() {
1430
+ this._collections.clear();
1431
+ this._vecs.clear();
1432
+ }
1433
+ };
1434
+
1435
+ // src/lib/languages.ts
1436
+ import path3 from "path";
1437
+ var SUPPORTED_EXTENSIONS = {
1438
+ // TypeScript / JavaScript
1439
+ ".ts": "typescript",
1440
+ ".tsx": "typescript",
1441
+ ".js": "javascript",
1442
+ ".jsx": "javascript",
1443
+ ".mjs": "javascript",
1444
+ ".cjs": "javascript",
1445
+ // Systems
1446
+ ".go": "go",
1447
+ ".rs": "rust",
1448
+ ".cpp": "cpp",
1449
+ ".cc": "cpp",
1450
+ ".c": "c",
1451
+ ".h": "c",
1452
+ ".hpp": "cpp",
1453
+ // JVM
1454
+ ".java": "java",
1455
+ ".kt": "kotlin",
1456
+ ".scala": "scala",
1457
+ // Scripting
1458
+ ".py": "python",
1459
+ ".rb": "ruby",
1460
+ ".php": "php",
1461
+ ".lua": "lua",
1462
+ ".sh": "bash",
1463
+ ".bash": "bash",
1464
+ ".zsh": "bash",
1465
+ // Web
1466
+ ".html": "html",
1467
+ ".css": "css",
1468
+ ".scss": "scss",
1469
+ ".less": "less",
1470
+ ".svelte": "svelte",
1471
+ ".vue": "vue",
1472
+ // Data / Config (JSON + YAML excluded — config/CI files cause search noise)
1473
+ ".toml": "toml",
1474
+ ".xml": "xml",
1475
+ ".graphql": "graphql",
1476
+ ".gql": "graphql",
1477
+ // Database
1478
+ ".sql": "sql",
1479
+ ".prisma": "prisma",
1480
+ // Other
1481
+ ".swift": "swift",
1482
+ ".dart": "dart",
1483
+ ".r": "r",
1484
+ ".ex": "elixir",
1485
+ ".exs": "elixir",
1486
+ ".erl": "erlang",
1487
+ ".zig": "zig"
1488
+ };
1489
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
1490
+ // Package managers
1491
+ "node_modules",
1492
+ "bower_components",
1493
+ ".pnpm",
1494
+ // Build output
1495
+ "dist",
1496
+ "build",
1497
+ "out",
1498
+ ".next",
1499
+ ".nuxt",
1500
+ ".output",
1501
+ ".svelte-kit",
1502
+ // Auto-generated code
1503
+ "generated",
1504
+ "sdk",
1505
+ "openapi",
1506
+ // Version control
1507
+ ".git",
1508
+ ".hg",
1509
+ ".svn",
1510
+ // IDE / Editor
1511
+ ".idea",
1512
+ ".vscode",
1513
+ // Runtime / Cache
1514
+ "__pycache__",
1515
+ ".pytest_cache",
1516
+ "venv",
1517
+ ".venv",
1518
+ ".env",
1519
+ ".tox",
1520
+ // Coverage / Test artifacts
1521
+ "coverage",
1522
+ ".nyc_output",
1523
+ "htmlcov",
1524
+ // Compiled
1525
+ "target",
1526
+ // Rust, Java
1527
+ ".cargo",
1528
+ "vendor",
1529
+ // Go, PHP
1530
+ // Database (auto-generated migrations, dumps, seeds)
1531
+ "migrations",
1532
+ "db_dumps",
1533
+ "seeds",
1534
+ // AI / Model cache
1535
+ ".model-cache",
1536
+ ".brainbank",
1537
+ // OS
1538
+ ".DS_Store"
1539
+ ]);
1540
+ var IGNORE_FILES = /* @__PURE__ */ new Set([
1541
+ "package-lock.json",
1542
+ "yarn.lock",
1543
+ "pnpm-lock.yaml",
1544
+ "bun.lockb",
1545
+ "Cargo.lock",
1546
+ "Gemfile.lock",
1547
+ "poetry.lock",
1548
+ "composer.lock",
1549
+ "go.sum"
1550
+ ]);
1551
+ function isSupported(filePath) {
1552
+ const ext = path3.extname(filePath).toLowerCase();
1553
+ return ext in SUPPORTED_EXTENSIONS;
1554
+ }
1555
+ __name(isSupported, "isSupported");
1556
+ function getLanguage(filePath) {
1557
+ const ext = path3.extname(filePath).toLowerCase();
1558
+ return SUPPORTED_EXTENSIONS[ext];
1559
+ }
1560
+ __name(getLanguage, "getLanguage");
1561
+ function isIgnoredDir(dirName) {
1562
+ return IGNORE_DIRS.has(dirName);
1563
+ }
1564
+ __name(isIgnoredDir, "isIgnoredDir");
1565
+ function isIgnoredFile(fileName) {
1566
+ return IGNORE_FILES.has(fileName);
1567
+ }
1568
+ __name(isIgnoredFile, "isIgnoredFile");
1569
+ function matchesGlob(relPath, patterns) {
1570
+ for (const pattern of patterns) {
1571
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\x00/g, ".*");
1572
+ if (new RegExp(`^${regex}$`).test(relPath)) return true;
1573
+ }
1574
+ return false;
1575
+ }
1576
+ __name(matchesGlob, "matchesGlob");
1577
+
1578
+ // src/services/watch.ts
1579
+ import * as fs3 from "fs";
1580
+ import * as path4 from "path";
1581
+ var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst"]);
1582
+ var Watcher = class {
1583
+ static {
1584
+ __name(this, "Watcher");
1585
+ }
1586
+ _active = true;
1587
+ _batches = /* @__PURE__ */ new Map();
1588
+ _reindexFn;
1589
+ _options;
1590
+ _keepalive = null;
1591
+ constructor(reindexFn, plugins, options = {}, repoPath) {
1592
+ this._reindexFn = reindexFn;
1593
+ this._options = options;
1594
+ this._startWatching(plugins, repoPath);
1595
+ }
1596
+ /** Whether the watcher is active. */
1597
+ get active() {
1598
+ return this._active;
1599
+ }
1600
+ /** Stop all plugin watchers. */
1601
+ async close() {
1602
+ this._active = false;
1603
+ if (this._keepalive) {
1604
+ clearInterval(this._keepalive);
1605
+ this._keepalive = null;
1606
+ }
1607
+ for (const batch of this._batches.values()) {
1608
+ if (batch.timer) clearTimeout(batch.timer);
1609
+ try {
1610
+ await batch.handle.stop();
1611
+ } catch (err) {
1612
+ this._options.onError?.(err instanceof Error ? err : new Error(String(err)));
1613
+ }
1614
+ }
1615
+ this._batches.clear();
1616
+ }
1617
+ /** Start watching for each WatchablePlugin, with shared fs.watch fallback. */
1618
+ _startWatching(plugins, repoPath) {
1619
+ let hasAnyWatcher = false;
1620
+ const fallbackPlugins = [];
1621
+ for (const plugin of plugins) {
1622
+ if (isWatchable(plugin)) {
1623
+ try {
1624
+ const handle = plugin.watch((event) => this._onEvent(plugin, event));
1625
+ this._batches.set(plugin.name, {
1626
+ plugin,
1627
+ handle,
1628
+ events: [],
1629
+ timer: null,
1630
+ flushing: false
1631
+ });
1632
+ hasAnyWatcher = true;
1633
+ } catch (err) {
1634
+ this._options.onError?.(err instanceof Error ? err : new Error(String(err)));
1635
+ }
1636
+ } else if (isIndexable(plugin) && repoPath) {
1637
+ fallbackPlugins.push(plugin);
1638
+ }
1639
+ }
1640
+ if (fallbackPlugins.length > 0 && repoPath) {
1641
+ const sharedHandle = this._startSharedFsWatch(fallbackPlugins, repoPath);
1642
+ if (sharedHandle) {
1643
+ for (const plugin of fallbackPlugins) {
1644
+ this._batches.set(plugin.name, {
1645
+ plugin,
1646
+ handle: sharedHandle,
1647
+ events: [],
1648
+ timer: null,
1649
+ flushing: false
1650
+ });
1651
+ }
1652
+ hasAnyWatcher = true;
1653
+ }
1654
+ }
1655
+ if (hasAnyWatcher) {
1656
+ this._keepalive = setInterval(() => {
1657
+ }, 6e4);
1658
+ this._keepalive.unref?.();
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Single shared recursive fs.watch that fans out events to multiple plugins.
1663
+ * Each event is routed based on: sub-repo prefix (code:backend → servicehub-backend/),
1664
+ * file extension (docs → .md only), and code support (code → isSupported).
1665
+ */
1666
+ _startSharedFsWatch(plugins, repoPath) {
1667
+ const watchers = [];
1668
+ const ignorePatterns = this._options.ignore ?? [];
1669
+ const recentEvents = /* @__PURE__ */ new Map();
1670
+ const DEDUP_MS = 100;
1671
+ const routes = plugins.map((plugin) => {
1672
+ const baseName = plugin.name.split(":")[0];
1673
+ const subRepo = plugin.name.includes(":") ? plugin.name.split(":").slice(1).join(":") : null;
1674
+ return { plugin, baseName, subRepo };
1675
+ });
1676
+ const watchDir = /* @__PURE__ */ __name((dir) => {
1677
+ try {
1678
+ const watcher = fs3.watch(dir, { persistent: true }, (_eventType, filename) => {
1679
+ if (!filename || !this._active) return;
1680
+ const fullPath = path4.join(dir, filename);
1681
+ const relPath = path4.relative(repoPath, fullPath);
1682
+ const ext = path4.extname(fullPath).toLowerCase();
1683
+ if (ignorePatterns.length > 0 && matchesGlob(relPath, ignorePatterns)) return;
1684
+ const now = Date.now();
1685
+ const lastSeen = recentEvents.get(relPath);
1686
+ if (lastSeen && now - lastSeen < DEDUP_MS) return;
1687
+ recentEvents.set(relPath, now);
1688
+ const event = {
1689
+ type: "update",
1690
+ sourceId: relPath,
1691
+ sourceName: "file"
1692
+ };
1693
+ for (const { plugin, baseName, subRepo } of routes) {
1694
+ if (subRepo && !relPath.startsWith(subRepo + "/")) continue;
1695
+ if (baseName === "docs") {
1696
+ if (!DOC_EXTENSIONS.has(ext)) continue;
1697
+ } else {
1698
+ if (!isSupported(fullPath)) continue;
1699
+ }
1700
+ this._onEvent(plugin, event);
1701
+ }
1702
+ });
1703
+ watcher.on("error", (err) => {
1704
+ this._options.onError?.(err instanceof Error ? err : new Error(String(err)));
1705
+ });
1706
+ watchers.push(watcher);
1707
+ } catch {
1708
+ }
1709
+ try {
1710
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
1711
+ if (!entry.isDirectory()) continue;
1712
+ if (isIgnoredDir(entry.name)) continue;
1713
+ if (entry.name.startsWith(".")) continue;
1714
+ const dirRel = path4.relative(repoPath, path4.join(dir, entry.name));
1715
+ if (ignorePatterns.length > 0 && matchesGlob(dirRel + "/", ignorePatterns)) continue;
1716
+ watchDir(path4.join(dir, entry.name));
1717
+ }
1718
+ } catch {
1719
+ }
1720
+ }, "watchDir");
1721
+ watchDir(repoPath);
1722
+ if (watchers.length === 0) return null;
1723
+ const cleanupInterval = setInterval(() => {
1724
+ const cutoff = Date.now() - 1e4;
1725
+ for (const [key, ts] of recentEvents) {
1726
+ if (ts < cutoff) recentEvents.delete(key);
1727
+ }
1728
+ }, 3e4);
1729
+ cleanupInterval.unref?.();
1730
+ let stopped = false;
1731
+ return {
1732
+ get active() {
1733
+ return !stopped;
1734
+ },
1735
+ async stop() {
1736
+ if (stopped) return;
1737
+ stopped = true;
1738
+ clearInterval(cleanupInterval);
1739
+ for (const w of watchers) {
1740
+ try {
1741
+ w.close();
1742
+ } catch {
1743
+ }
1744
+ }
1745
+ watchers.length = 0;
1746
+ }
1747
+ };
1748
+ }
1749
+ /** Handle an incoming event from a plugin. */
1750
+ _onEvent(plugin, event) {
1751
+ if (!this._active) return;
1752
+ const batch = this._batches.get(plugin.name);
1753
+ if (!batch) return;
1754
+ batch.events.push(event);
1755
+ const pluginDebounce = isWatchable(plugin) ? plugin.watchConfig?.()?.debounceMs : void 0;
1756
+ const debounceMs = pluginDebounce ?? this._options.debounceMs ?? 2e3;
1757
+ const batchSize = isWatchable(plugin) ? plugin.watchConfig?.()?.batchSize : void 0;
1758
+ const shouldFlushNow = debounceMs === 0 || batchSize !== void 0 && batch.events.length >= batchSize;
1759
+ if (shouldFlushNow) {
1760
+ if (batch.timer) clearTimeout(batch.timer);
1761
+ batch.timer = null;
1762
+ void this._flush(batch);
1763
+ return;
1764
+ }
1765
+ if (batch.timer) clearTimeout(batch.timer);
1766
+ batch.timer = setTimeout(() => void this._flush(batch), debounceMs);
1767
+ }
1768
+ /** Flush pending events for a plugin — trigger re-indexing. */
1769
+ async _flush(batch) {
1770
+ if (batch.flushing || batch.events.length === 0) return;
1771
+ batch.flushing = true;
1772
+ const { onIndex, onError } = this._options;
1773
+ try {
1774
+ const events = [...batch.events];
1775
+ batch.events.length = 0;
1776
+ const ids = events.map((e) => e.sourceId);
1777
+ if (isIndexable(batch.plugin) && batch.plugin.indexItems) {
1778
+ await batch.plugin.indexItems(ids);
1779
+ for (const id of ids) {
1780
+ onIndex?.(id, batch.plugin.name);
1781
+ }
1782
+ } else if (isIndexable(batch.plugin)) {
1783
+ await batch.plugin.index();
1784
+ for (const id of ids) {
1785
+ onIndex?.(id, batch.plugin.name);
1786
+ }
1787
+ } else {
1788
+ await this._reindexFn();
1789
+ for (const id of ids) {
1790
+ onIndex?.(id, batch.plugin.name);
1791
+ }
1792
+ }
1793
+ } catch (err) {
1794
+ onError?.(err instanceof Error ? err : new Error(String(err)));
1795
+ } finally {
1796
+ batch.flushing = false;
1797
+ if (batch.events.length > 0 && this._active) {
1798
+ const debounceMs = this._options.debounceMs ?? 2e3;
1799
+ batch.timer = setTimeout(() => void this._flush(batch), debounceMs);
1800
+ }
1801
+ }
1802
+ }
1803
+ };
1804
+
1805
+ // src/services/webhook-server.ts
1806
+ import * as http from "http";
1807
+ var WebhookServer = class {
1808
+ static {
1809
+ __name(this, "WebhookServer");
1810
+ }
1811
+ _server = null;
1812
+ _routes = [];
1813
+ _listening = false;
1814
+ /** Start listening on the specified port. */
1815
+ listen(port) {
1816
+ if (this._listening) return;
1817
+ this._server = http.createServer((req, res) => {
1818
+ this._handleRequest(req, res);
1819
+ });
1820
+ this._server.listen(port);
1821
+ this._listening = true;
1822
+ }
1823
+ /** Register a webhook route for a plugin. */
1824
+ register(pluginName, path9, handler) {
1825
+ const normalizedPath = path9.startsWith("/") ? path9 : `/${path9}`;
1826
+ this._routes.push({ pluginName, path: normalizedPath, handler });
1827
+ }
1828
+ /** Remove all routes for a plugin. */
1829
+ unregister(pluginName) {
1830
+ this._routes = this._routes.filter((r) => r.pluginName !== pluginName);
1831
+ }
1832
+ /** Stop the server and clear all routes. */
1833
+ close() {
1834
+ this._server?.close();
1835
+ this._server = null;
1836
+ this._routes = [];
1837
+ this._listening = false;
1838
+ }
1839
+ /** Whether the server is currently listening. */
1840
+ get active() {
1841
+ return this._listening;
1842
+ }
1843
+ /** Route incoming POST requests to the matching handler. */
1844
+ _handleRequest(req, res) {
1845
+ if (req.method !== "POST") {
1846
+ res.writeHead(405, { "Content-Type": "application/json" });
1847
+ res.end(JSON.stringify({ error: "Method not allowed" }));
1848
+ return;
1849
+ }
1850
+ const route = this._routes.find((r) => req.url === r.path);
1851
+ if (!route) {
1852
+ res.writeHead(404, { "Content-Type": "application/json" });
1853
+ res.end(JSON.stringify({ error: "Not found" }));
1854
+ return;
1855
+ }
1856
+ const chunks = [];
1857
+ req.on("data", (chunk) => chunks.push(chunk));
1858
+ req.on("end", () => {
1859
+ try {
1860
+ const raw = Buffer.concat(chunks).toString("utf8");
1861
+ const body = raw ? JSON.parse(raw) : {};
1862
+ route.handler(body);
1863
+ res.writeHead(200, { "Content-Type": "application/json" });
1864
+ res.end(JSON.stringify({ ok: true }));
1865
+ } catch {
1866
+ res.writeHead(400, { "Content-Type": "application/json" });
1867
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
1868
+ }
1869
+ });
1870
+ }
1871
+ };
1872
+
1873
+ // src/brainbank.ts
1874
+ import { EventEmitter } from "events";
1875
+ import * as path5 from "path";
1876
+
1877
+ // src/providers/vector/hnsw-loader.ts
1878
+ import { dirname as dirname2, join as join4 } from "path";
1879
+ function hnswPath(dbPath, name) {
1880
+ return join4(dirname2(dbPath), `hnsw-${name}.index`);
1881
+ }
1882
+ __name(hnswPath, "hnswPath");
1883
+ function lockDir(dbPath) {
1884
+ return dirname2(dbPath);
1885
+ }
1886
+ __name(lockDir, "lockDir");
1887
+ function countRows(db, table) {
1888
+ const row = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get();
1889
+ return row?.c ?? 0;
1890
+ }
1891
+ __name(countRows, "countRows");
1892
+ async function saveAllHnsw(dbPath, kvHnsw, sharedHnsw, privateHnsw) {
1893
+ try {
1894
+ await withLock(lockDir(dbPath), "hnsw", () => {
1895
+ kvHnsw.save(hnswPath(dbPath, "kv"));
1896
+ for (const [name, { hnsw }] of sharedHnsw) {
1897
+ hnsw.save(hnswPath(dbPath, name));
1898
+ }
1899
+ for (const [name, hnsw] of privateHnsw) {
1900
+ hnsw.save(hnswPath(dbPath, name));
1901
+ }
1902
+ });
1903
+ return true;
1904
+ } catch {
1905
+ return false;
1906
+ }
1907
+ }
1908
+ __name(saveAllHnsw, "saveAllHnsw");
1909
+ function loadVectors(db, table, idCol, hnsw, cache) {
1910
+ const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
1911
+ for (const row of iter) {
1912
+ const vec = new Float32Array(
1913
+ row.embedding.buffer.slice(
1914
+ row.embedding.byteOffset,
1915
+ row.embedding.byteOffset + row.embedding.byteLength
1916
+ )
1917
+ );
1918
+ hnsw.add(vec, row[idCol]);
1919
+ cache.set(row[idCol], vec);
1920
+ }
1921
+ }
1922
+ __name(loadVectors, "loadVectors");
1923
+ function loadVecCache(db, table, idCol, cache) {
1924
+ const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
1925
+ for (const row of iter) {
1926
+ const vec = new Float32Array(
1927
+ row.embedding.buffer.slice(
1928
+ row.embedding.byteOffset,
1929
+ row.embedding.byteOffset + row.embedding.byteLength
1930
+ )
1931
+ );
1932
+ cache.set(row[idCol], vec);
1933
+ }
1934
+ }
1935
+ __name(loadVecCache, "loadVecCache");
1936
+ function reloadHnsw(deps) {
1937
+ const { dbPath, db, name, hnsw, vecCache, vectorTable, idCol } = deps;
1938
+ const indexPath = hnswPath(dbPath, name);
1939
+ const rowCount = countRows(db, vectorTable);
1940
+ hnsw.reinit();
1941
+ vecCache.clear();
1942
+ if (hnsw.tryLoad(indexPath, rowCount)) {
1943
+ loadVecCache(db, vectorTable, idCol, vecCache);
1944
+ } else {
1945
+ loadVectors(db, vectorTable, idCol, hnsw, vecCache);
1946
+ }
1947
+ }
1948
+ __name(reloadHnsw, "reloadHnsw");
1949
+
1950
+ // src/engine/index-api.ts
1951
+ function mergeResult(acc, r) {
1952
+ if (!acc) return { ...r };
1953
+ return {
1954
+ indexed: acc.indexed + r.indexed,
1955
+ skipped: acc.skipped + r.skipped,
1956
+ chunks: (acc.chunks ?? 0) + (r.chunks ?? 0)
1957
+ };
1958
+ }
1959
+ __name(mergeResult, "mergeResult");
1960
+ async function runIndex(deps, options = {}) {
1961
+ const want = options.modules ? new Set(options.modules) : null;
1962
+ const results = {};
1963
+ for (const mod of deps.registry.all) {
1964
+ const baseType = mod.name.split(":")[0];
1965
+ if (want && !want.has(baseType)) continue;
1966
+ if (!isIndexable(mod)) continue;
1967
+ const label = mod.name;
1968
+ options.onProgress?.(label, "Starting...");
1969
+ const r = await mod.index({
1970
+ forceReindex: options.forceReindex,
1971
+ onProgress: /* @__PURE__ */ __name((msg, cur, total) => options.onProgress?.(label, `[${cur}/${total}] ${msg}`), "onProgress"),
1972
+ ...options.pluginOptions
1973
+ });
1974
+ results[baseType] = mergeResult(results[baseType], r);
1975
+ bumpVersion(deps.db, mod.name);
1976
+ }
1977
+ await saveAllHnsw(
1978
+ deps.dbPath,
1979
+ deps.kvHnsw,
1980
+ deps.sharedHnsw,
1981
+ /* @__PURE__ */ new Map()
1982
+ );
1983
+ deps.emit("indexed", results);
1984
+ return results;
1985
+ }
1986
+ __name(runIndex, "runIndex");
1987
+
1988
+ // src/engine/reembed.ts
1989
+ var CORE_TABLES = [
1990
+ {
1991
+ name: "kv",
1992
+ textTable: "kv_data",
1993
+ vectorTable: "kv_vectors",
1994
+ idColumn: "id",
1995
+ fkColumn: "data_id",
1996
+ textBuilder: /* @__PURE__ */ __name((r) => String(r.content), "textBuilder")
1997
+ }
1998
+ ];
1999
+ function collectTables(plugins) {
2000
+ const byVectorTable = /* @__PURE__ */ new Map();
2001
+ for (const p of plugins) {
2002
+ if (isReembeddable(p)) {
2003
+ const config = p.reembedConfig();
2004
+ byVectorTable.set(config.vectorTable, config);
2005
+ }
2006
+ }
2007
+ for (const t of CORE_TABLES) {
2008
+ byVectorTable.set(t.vectorTable, t);
2009
+ }
2010
+ return [...byVectorTable.values()];
2011
+ }
2012
+ __name(collectTables, "collectTables");
2013
+ async function reembedAll(db, embedding, hnswMap, plugins, options = {}, persist) {
2014
+ const { batchSize = 50, onProgress } = options;
2015
+ const tables = collectTables(plugins);
2016
+ const counts = {};
2017
+ let total = 0;
2018
+ for (const table of tables) {
2019
+ try {
2020
+ const textExists = db.prepare(
2021
+ `SELECT COUNT(*) as c FROM sqlite_master WHERE type='table' AND name=?`
2022
+ ).get(table.textTable).c;
2023
+ const vecExists = db.prepare(
2024
+ `SELECT COUNT(*) as c FROM sqlite_master WHERE type='table' AND name=?`
2025
+ ).get(table.vectorTable).c;
2026
+ if (!textExists || !vecExists) continue;
2027
+ } catch (e) {
2028
+ if (e instanceof Error && e.message.includes("no such table")) continue;
2029
+ throw e;
2030
+ }
2031
+ const count = await reembedTable(db, embedding, table, batchSize, onProgress);
2032
+ counts[table.name] = count;
2033
+ total += count;
2034
+ const entry = hnswMap.get(table.name);
2035
+ if (entry && count > 0) {
2036
+ await rebuildHnsw(db, table, entry.hnsw, entry.vecs);
2037
+ }
2038
+ }
2039
+ setEmbeddingMeta(db, embedding);
2040
+ if (persist) {
2041
+ saveAllHnsw(persist.dbPath, persist.kvHnsw, persist.sharedHnsw, /* @__PURE__ */ new Map());
2042
+ }
2043
+ return {
2044
+ counts,
2045
+ total
2046
+ };
2047
+ }
2048
+ __name(reembedAll, "reembedAll");
2049
+ async function reembedTable(db, embedding, table, batchSize, onProgress) {
2050
+ const totalCount = db.prepare(
2051
+ `SELECT COUNT(*) as c FROM ${table.textTable}`
2052
+ ).get().c;
2053
+ if (totalCount === 0) return 0;
2054
+ const tempTable = `_reembed_${table.vectorTable}`;
2055
+ db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
2056
+ db.exec(`CREATE TABLE ${tempTable} AS SELECT * FROM ${table.vectorTable} WHERE 0`);
2057
+ const insertTemp = db.prepare(
2058
+ `INSERT INTO ${tempTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
2059
+ );
2060
+ let processed = 0;
2061
+ try {
2062
+ for (let offset = 0; offset < totalCount; offset += batchSize) {
2063
+ const batch = db.prepare(
2064
+ `SELECT * FROM ${table.textTable} LIMIT ? OFFSET ?`
2065
+ ).all(batchSize, offset);
2066
+ const texts = batch.map((r) => table.textBuilder(r));
2067
+ const vectors = await embedding.embedBatch(texts);
2068
+ db.transaction(() => {
2069
+ for (let j = 0; j < batch.length; j++) {
2070
+ insertTemp.run(batch[j][table.idColumn], vecToBuffer(vectors[j]));
2071
+ }
2072
+ });
2073
+ processed += batch.length;
2074
+ onProgress?.(table.name, processed, totalCount);
2075
+ }
2076
+ db.transaction(() => {
2077
+ db.exec(`DELETE FROM ${table.vectorTable}`);
2078
+ db.exec(`INSERT INTO ${table.vectorTable} SELECT * FROM ${tempTable}`);
2079
+ });
2080
+ } finally {
2081
+ db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
2082
+ }
2083
+ return processed;
2084
+ }
2085
+ __name(reembedTable, "reembedTable");
2086
+ async function rebuildHnsw(db, table, hnsw, vecs) {
2087
+ vecs.clear();
2088
+ hnsw.reinit();
2089
+ const rows = db.prepare(
2090
+ `SELECT ${table.fkColumn} as id, embedding FROM ${table.vectorTable}`
2091
+ ).all();
2092
+ for (const row of rows) {
2093
+ const buf = Buffer.from(row.embedding);
2094
+ const vec = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
2095
+ hnsw.add(vec, row.id);
2096
+ vecs.set(row.id, vec);
2097
+ }
2098
+ }
2099
+ __name(rebuildHnsw, "rebuildHnsw");
2100
+
2101
+ // src/engine/search-api.ts
2102
+ function createSearchAPI(_db, embedding, config, registry, kvService, sharedHnsw) {
2103
+ const strategies = /* @__PURE__ */ new Map();
2104
+ for (const mod of registry.all) {
2105
+ if (isVectorSearchPlugin(mod)) {
2106
+ const vs = mod.createVectorSearch();
2107
+ if (vs) {
2108
+ strategies.set(mod.name, vs);
2109
+ }
2110
+ }
2111
+ }
2112
+ const search = strategies.size > 0 ? new CompositeVectorSearch({
2113
+ strategies,
2114
+ embedding
2115
+ }) : void 0;
2116
+ const bm25 = new CompositeBM25Search(registry);
2117
+ const rerankerName = config.reranker ? config.reranker.constructor?.name ?? "custom" : void 0;
2118
+ const contextBuilder = new ContextBuilder(search, registry, config.pruner, embedding, rerankerName, config.contextFields ?? {}, config.expander);
2119
+ return new SearchAPI({
2120
+ search,
2121
+ bm25,
2122
+ registry,
2123
+ config,
2124
+ kvService,
2125
+ contextBuilder,
2126
+ embedding
2127
+ });
2128
+ }
2129
+ __name(createSearchAPI, "createSearchAPI");
2130
+ var SearchAPI = class {
2131
+ constructor(_d) {
2132
+ this._d = _d;
2133
+ }
2134
+ static {
2135
+ __name(this, "SearchAPI");
2136
+ }
2137
+ /** Build formatted context block for LLM injection. */
2138
+ async getContext(task, options = {}) {
2139
+ if (!this._d.contextBuilder) return "";
2140
+ return this._d.contextBuilder.build(task, options);
2141
+ }
2142
+ /** Semantic search across all loaded modules. */
2143
+ async search(query, options) {
2144
+ const t0 = Date.now();
2145
+ const lists = [];
2146
+ if (this._d.search) {
2147
+ lists.push(await this._d.search.search(query, options));
2148
+ }
2149
+ lists.push(...await this._collectSearchablePlugins(query, options));
2150
+ let results;
2151
+ if (lists.length === 0) results = [];
2152
+ else if (lists.length === 1) results = lists[0];
2153
+ else results = reciprocalRankFusion(lists);
2154
+ this._logSearch("search", query, options, results, Date.now() - t0);
2155
+ return results;
2156
+ }
2157
+ /** Hybrid search: vector + BM25 → RRF. */
2158
+ async hybridSearch(query, options) {
2159
+ const t0 = Date.now();
2160
+ const src = options?.sources ?? {};
2161
+ const lists = [];
2162
+ if (this._d.search) {
2163
+ const [vec, kw] = await Promise.all([
2164
+ this._d.search.search(query, options),
2165
+ Promise.resolve(this._d.bm25?.search(query, options) ?? [])
2166
+ ]);
2167
+ lists.push(vec, kw);
2168
+ }
2169
+ lists.push(...await this._collectSearchablePlugins(query, options));
2170
+ lists.push(...await this._collectKvCollections(query, src));
2171
+ let results;
2172
+ if (lists.length === 0) results = [];
2173
+ else {
2174
+ const fused = reciprocalRankFusion(lists);
2175
+ if (this._d.config.reranker && fused.length > 1) {
2176
+ results = await rerank(query, fused, this._d.config.reranker);
2177
+ } else {
2178
+ results = fused;
2179
+ }
2180
+ }
2181
+ this._logSearch("hybridSearch", query, options, results, Date.now() - t0);
2182
+ return results;
2183
+ }
2184
+ /** BM25 keyword search only. */
2185
+ async searchBM25(query, options) {
2186
+ const t0 = Date.now();
2187
+ const results = await this._d.bm25?.search(query, options) ?? [];
2188
+ this._logSearch("searchBM25", query, options, results, Date.now() - t0);
2189
+ return results;
2190
+ }
2191
+ /** Rebuild FTS5 indices. */
2192
+ rebuildFTS() {
2193
+ this._d.bm25?.rebuild?.();
2194
+ }
2195
+ /** Collect results from all SearchablePlugins (docs, custom). */
2196
+ async _collectSearchablePlugins(query, options) {
2197
+ const lists = [];
2198
+ for (const mod of this._d.registry.all) {
2199
+ if (!isSearchable(mod)) continue;
2200
+ if (isVectorSearchPlugin(mod)) continue;
2201
+ const hits = await mod.search(query, options ? { ...options } : void 0);
2202
+ if (hits.length > 0) lists.push(hits);
2203
+ }
2204
+ return lists;
2205
+ }
2206
+ /** Collect results from KV collections named in sources. */
2207
+ async _collectKvCollections(query, sources) {
2208
+ const pluginNames = new Set(this._d.registry.names.map((n) => n.split(":")[0]));
2209
+ const lists = [];
2210
+ for (const [name, k] of Object.entries(sources)) {
2211
+ if (pluginNames.has(name)) continue;
2212
+ const hits = await this._d.kvService.collection(name).searchAsResults(query, k);
2213
+ if (hits.length > 0) lists.push(hits);
2214
+ }
2215
+ return lists;
2216
+ }
2217
+ /** Log a search/hybridSearch/searchBM25 call. */
2218
+ _logSearch(method, query, options, results, durationMs) {
2219
+ logQuery({
2220
+ source: options?.source ?? "api",
2221
+ method,
2222
+ query,
2223
+ embedding: providerKey(this._d.embedding),
2224
+ pruner: null,
2225
+ reranker: this._d.config.reranker ? this._d.config.reranker.constructor?.name ?? "custom" : null,
2226
+ options: {
2227
+ sources: options?.sources,
2228
+ minScore: options?.minScore
2229
+ },
2230
+ results: results.map(_toLogResult2),
2231
+ durationMs
2232
+ });
2233
+ }
2234
+ };
2235
+ function _toLogResult2(r) {
2236
+ const meta = r.metadata;
2237
+ return {
2238
+ filePath: r.filePath ?? "unknown",
2239
+ score: r.score,
2240
+ type: r.type,
2241
+ name: meta?.name ?? void 0
2242
+ };
2243
+ }
2244
+ __name(_toLogResult2, "_toLogResult");
2245
+
2246
+ // src/services/plugin-registry.ts
2247
+ var ALIASES = {};
2248
+ var PluginRegistry = class {
2249
+ static {
2250
+ __name(this, "PluginRegistry");
2251
+ }
2252
+ _map = /* @__PURE__ */ new Map();
2253
+ /** Store a plugin. Duplicate names silently overwrite. */
2254
+ register(plugin) {
2255
+ this._map.set(plugin.name, plugin);
2256
+ }
2257
+ /**
2258
+ * Check whether a plugin is registered.
2259
+ * Supports type-prefix matching: `has('code')` returns true if
2260
+ * 'code', 'code:frontend', or 'code:backend' is registered.
2261
+ */
2262
+ has(name) {
2263
+ if (this._map.has(name)) return true;
2264
+ for (const key of this._map.keys()) {
2265
+ if (key.startsWith(name + ":")) return true;
2266
+ }
2267
+ return false;
2268
+ }
2269
+ /**
2270
+ * Get a plugin by name. Throws a descriptive error if not found.
2271
+ *
2272
+ * Resolution order:
2273
+ * 1. Alias map (currently empty)
2274
+ * 2. Exact match
2275
+ * 3. First type-prefix match ('code' → 'code:frontend')
2276
+ */
2277
+ get(name) {
2278
+ const resolved = ALIASES[name] ?? name;
2279
+ const exact = this._map.get(resolved);
2280
+ if (exact) return exact;
2281
+ const prefixed = this.firstByType(name);
2282
+ if (prefixed) return prefixed;
2283
+ throw new Error(
2284
+ `BrainBank: Plugin '${name}' is not loaded. Add .use(${name}()) to your BrainBank instance.`
2285
+ );
2286
+ }
2287
+ /**
2288
+ * Return every plugin whose name equals `type` or starts with `type + ':'`.
2289
+ * Example: allByType('code') → [code, code:frontend, code:backend]
2290
+ */
2291
+ allByType(type) {
2292
+ return [...this._map.values()].filter(
2293
+ (m) => m.name === type || m.name.startsWith(type + ":")
2294
+ );
2295
+ }
2296
+ /** Return the first plugin that matches the type prefix, or undefined. */
2297
+ firstByType(type) {
2298
+ for (const m of this._map.values()) {
2299
+ if (m.name === type || m.name.startsWith(type + ":")) return m;
2300
+ }
2301
+ return void 0;
2302
+ }
2303
+ /** All registered plugin names (insertion order). */
2304
+ get names() {
2305
+ return [...this._map.keys()];
2306
+ }
2307
+ /** All registered plugin instances (insertion order). */
2308
+ get all() {
2309
+ return [...this._map.values()];
2310
+ }
2311
+ /**
2312
+ * Underlying Map.
2313
+ * Prefer `all`, `allByType`, or `firstByType` everywhere else.
2314
+ */
2315
+ get raw() {
2316
+ return this._map;
2317
+ }
2318
+ /** Remove all registered plugins. Called by BrainBank.close(). */
2319
+ clear() {
2320
+ this._map.clear();
2321
+ }
2322
+ };
2323
+
2324
+ // src/brainbank.ts
2325
+ var BrainBank = class extends EventEmitter {
2326
+ static {
2327
+ __name(this, "BrainBank");
2328
+ }
2329
+ _config;
2330
+ _db;
2331
+ _embedding;
2332
+ _registry = new PluginRegistry();
2333
+ _searchAPI;
2334
+ _indexDeps;
2335
+ _kvService;
2336
+ _initialized = false;
2337
+ _initPromise = null;
2338
+ _watcher;
2339
+ _webhookServer;
2340
+ _sharedHnsw = /* @__PURE__ */ new Map();
2341
+ _repoDBs = /* @__PURE__ */ new Map();
2342
+ _loadedVersions = /* @__PURE__ */ new Map();
2343
+ constructor(config = {}) {
2344
+ super();
2345
+ this._config = resolveConfig(config);
2346
+ }
2347
+ /** Whether the brainbank has been initialized. */
2348
+ get isInitialized() {
2349
+ return this._initialized;
2350
+ }
2351
+ /** The resolved configuration. */
2352
+ get config() {
2353
+ return this._config;
2354
+ }
2355
+ /** All registered plugin names (insertion order). */
2356
+ get plugins() {
2357
+ return this._registry.names;
2358
+ }
2359
+ /**
2360
+ * Register a plugin. Chainable.
2361
+ *
2362
+ * @example
2363
+ * brain.use(code({ repoPath: '.' })).use(docs());
2364
+ *
2365
+ * @throws If called after `initialize()`.
2366
+ */
2367
+ use(plugin) {
2368
+ if (this._initialized) {
2369
+ throw new Error(
2370
+ `BrainBank: Cannot add plugin '${plugin.name}' after initialization. Call .use() before any operations.`
2371
+ );
2372
+ }
2373
+ this._registry.register(plugin);
2374
+ return this;
2375
+ }
2376
+ /**
2377
+ * Check if a plugin is loaded.
2378
+ * Also matches type prefix (e.g. `'code'` matches `'code:frontend'`).
2379
+ */
2380
+ has(name) {
2381
+ return this._registry.has(name);
2382
+ }
2383
+ /** Get a plugin instance by name. Returns `undefined` if not loaded. */
2384
+ plugin(name) {
2385
+ return this._registry.has(name) ? this._registry.get(name) : void 0;
2386
+ }
2387
+ /**
2388
+ * Initialize database, HNSW indices, and load existing vectors.
2389
+ * Automatically called by `index` / `search` methods if not yet initialized.
2390
+ * Concurrent calls are deduped via `_initPromise`.
2391
+ *
2392
+ * @param options.force - If `true`, skip vector load on dimension mismatch.
2393
+ */
2394
+ async initialize(options = {}) {
2395
+ if (this._initialized) return;
2396
+ if (this._initPromise) return this._initPromise;
2397
+ this._initPromise = this._runInitialize(options).then(() => {
2398
+ this._initPromise = null;
2399
+ }).catch((err) => {
2400
+ this._cleanupAfterFailedInit();
2401
+ throw err;
2402
+ });
2403
+ return this._initPromise;
2404
+ }
2405
+ /**
2406
+ * Estimated memory footprint of loaded HNSW indices (bytes).
2407
+ * Counts only vector data: `vectorCount × dims × 4`.
2408
+ * Returns 0 if not initialized.
2409
+ */
2410
+ memoryHint() {
2411
+ if (!this._initialized) return 0;
2412
+ const dims = this._config.embeddingDims;
2413
+ const bytesPerVector = dims * 4;
2414
+ let total = 0;
2415
+ if (this._kvService) total += this._kvService.hnsw.size * bytesPerVector;
2416
+ for (const { hnsw } of this._sharedHnsw.values()) {
2417
+ total += hnsw.size * bytesPerVector;
2418
+ }
2419
+ return total;
2420
+ }
2421
+ /** Close database and release all resources. Synchronous. */
2422
+ close() {
2423
+ void this._watcher?.close();
2424
+ this._webhookServer?.close();
2425
+ for (const plugin of this._registry.all) plugin.close?.();
2426
+ const reranker = this._config.reranker;
2427
+ reranker?.close?.();
2428
+ const pruner = this._config.pruner;
2429
+ pruner?.close?.();
2430
+ this._embedding?.close().catch(() => {
2431
+ });
2432
+ for (const db of this._repoDBs.values()) db.close();
2433
+ this._repoDBs.clear();
2434
+ this._db?.close();
2435
+ this._initialized = false;
2436
+ this._kvService?.clear();
2437
+ this._sharedHnsw.clear();
2438
+ this._loadedVersions.clear();
2439
+ this._kvService = void 0;
2440
+ this._searchAPI = void 0;
2441
+ this._indexDeps = void 0;
2442
+ this._webhookServer = void 0;
2443
+ this._registry.clear();
2444
+ }
2445
+ /**
2446
+ * Get or create a dynamic collection (universal KV primitive).
2447
+ *
2448
+ * @example
2449
+ * const errors = brain.collection('debug_errors');
2450
+ * await errors.add('Fixed null check', { file: 'api.ts' });
2451
+ * const hits = await errors.search('null pointer');
2452
+ *
2453
+ * @throws If not initialized.
2454
+ */
2455
+ collection(name) {
2456
+ if (!this._kvService) {
2457
+ throw new Error("BrainBank: Collections not ready. Call await brain.initialize() first.");
2458
+ }
2459
+ return this._kvService.collection(name);
2460
+ }
2461
+ /** List all collection names that have data. */
2462
+ listCollectionNames() {
2463
+ this._requireInit("listCollectionNames");
2464
+ return this._kvService.listNames();
2465
+ }
2466
+ /** Delete a collection's data and evict from cache. */
2467
+ deleteCollection(name) {
2468
+ this._requireInit("deleteCollection");
2469
+ this._kvService.delete(name);
2470
+ }
2471
+ /** Run indexing across selected modules. Auto-initializes. */
2472
+ async index(options = {}) {
2473
+ await this.initialize();
2474
+ return runIndex(this._indexDeps, options);
2475
+ }
2476
+ /**
2477
+ * Detect stale HNSW indices and hot-reload from disk.
2478
+ * Called implicitly before every search operation.
2479
+ * Cost: one SQLite SELECT (~5μs on WAL mode).
2480
+ */
2481
+ async ensureFresh() {
2482
+ if (!this._initialized) return;
2483
+ const dbVersions = getVersions(this._db);
2484
+ for (const [name, dbVersion] of dbVersions) {
2485
+ const loaded = this._loadedVersions.get(name) ?? 0;
2486
+ if (dbVersion <= loaded) continue;
2487
+ this.emit("progress", `Hot-reload: ${name} version ${loaded} \u2192 ${dbVersion}`);
2488
+ this._reloadIndex(name);
2489
+ this._loadedVersions.set(name, dbVersion);
2490
+ }
2491
+ }
2492
+ /**
2493
+ * Semantic search across all loaded modules.
2494
+ * Scope via `sources: { code: 10, git: 0 }`.
2495
+ */
2496
+ async search(query, options) {
2497
+ await this.initialize();
2498
+ await this.ensureFresh();
2499
+ return this._searchAPI?.search(query, options) ?? [];
2500
+ }
2501
+ /**
2502
+ * Hybrid search: vector + BM25 fused with Reciprocal Rank Fusion.
2503
+ * Scope via `sources: { code: 10, git: 5, docs: 3, myNotes: 5 }`.
2504
+ */
2505
+ async hybridSearch(query, options) {
2506
+ await this.initialize();
2507
+ await this.ensureFresh();
2508
+ return this._searchAPI?.hybridSearch(query, options) ?? [];
2509
+ }
2510
+ /** BM25 keyword search only (no embeddings needed). */
2511
+ async searchBM25(query, options) {
2512
+ await this.initialize();
2513
+ await this.ensureFresh();
2514
+ return this._searchAPI?.searchBM25(query, options) ?? [];
2515
+ }
2516
+ /** Build formatted context block for LLM system prompt injection. Auto-initializes. */
2517
+ async getContext(task, options = {}) {
2518
+ await this.initialize();
2519
+ await this.ensureFresh();
2520
+ return this._searchAPI?.getContext(task, options) ?? "";
2521
+ }
2522
+ /**
2523
+ * Resolve file paths, directories, and glob patterns to full SearchResults.
2524
+ * Bypasses search entirely — reads directly from plugin indexes.
2525
+ *
2526
+ * @example
2527
+ * const files = brain.resolveFiles(['src/auth/login.ts', 'src/graph/']);
2528
+ */
2529
+ resolveFiles(patterns) {
2530
+ this._requireInit("resolveFiles");
2531
+ const results = [];
2532
+ for (const mod of this._registry.all) {
2533
+ if (!isFileResolvable(mod)) continue;
2534
+ results.push(...mod.resolveFiles(patterns));
2535
+ }
2536
+ return results;
2537
+ }
2538
+ /** Rebuild FTS5 indices. */
2539
+ rebuildFTS() {
2540
+ this._requireInit("rebuildFTS");
2541
+ this._searchAPI?.rebuildFTS();
2542
+ }
2543
+ /** Get statistics for all loaded plugins. */
2544
+ stats() {
2545
+ this._requireInit("stats");
2546
+ const result = {};
2547
+ for (const mod of this._registry.all) {
2548
+ if (mod.stats) {
2549
+ const baseType = mod.name.split(":")[0];
2550
+ result[baseType] = mod.stats();
2551
+ }
2552
+ }
2553
+ return result;
2554
+ }
2555
+ /** Start watching for changes and auto-re-index. */
2556
+ watch(options = {}) {
2557
+ this._requireInit("watch");
2558
+ void this._watcher?.close();
2559
+ this._watcher = new Watcher(
2560
+ async () => {
2561
+ await this.index();
2562
+ },
2563
+ this._registry.all,
2564
+ options,
2565
+ this._config.repoPath
2566
+ );
2567
+ return this._watcher;
2568
+ }
2569
+ /**
2570
+ * Re-embed all existing text with the current embedding provider.
2571
+ * Use after switching providers (e.g. Local → OpenAI).
2572
+ */
2573
+ async reembed(options = {}) {
2574
+ await this.initialize();
2575
+ const hnswMap = /* @__PURE__ */ new Map();
2576
+ if (this._kvService) {
2577
+ hnswMap.set(HNSW.KV, { hnsw: this._kvService.hnsw, vecs: this._kvService.vecs });
2578
+ }
2579
+ for (const [type, shared] of this._sharedHnsw) {
2580
+ hnswMap.set(type, { hnsw: shared.hnsw, vecs: shared.vecCache });
2581
+ }
2582
+ const result = await reembedAll(this._db, this._embedding, hnswMap, this._registry.all, options, {
2583
+ dbPath: this._config.dbPath,
2584
+ kvHnsw: this._kvService.hnsw,
2585
+ sharedHnsw: this._sharedHnsw
2586
+ });
2587
+ this.emit("reembedded", result);
2588
+ return result;
2589
+ }
2590
+ /**
2591
+ * Linear 8-step initialization:
2592
+ * 1. Open database
2593
+ * 2. Resolve embedding provider
2594
+ * 3. Check dimension mismatch
2595
+ * 4. Create KV HNSW + KVService
2596
+ * 5. Load KV vectors
2597
+ * 6. Initialize plugins
2598
+ * 7. Persist HNSW indices
2599
+ * 8. Build SearchAPI + index deps
2600
+ */
2601
+ async _runInitialize(options = {}) {
2602
+ if (this._initialized) return;
2603
+ this._db = new SQLiteAdapter(this._config.dbPath);
2604
+ this._embedding = await this._resolveEmbedding();
2605
+ const mismatch = detectProviderMismatch(this._db, this._embedding);
2606
+ if (mismatch?.mismatch && !options.force) {
2607
+ this._db.close();
2608
+ throw new Error(
2609
+ `BrainBank: Embedding dimension mismatch (stored: ${mismatch.stored}, current: ${mismatch.current}). Run brain.reembed() to re-index with the new provider, or switch back to the original provider.`
2610
+ );
2611
+ }
2612
+ setEmbeddingMeta(this._db, this._embedding);
2613
+ const skipVectorLoad = !!(options.force && mismatch?.mismatch);
2614
+ const dims = this._embedding.dims ?? this._config.embeddingDims;
2615
+ const kvHnsw = new HNSWIndex(
2616
+ dims,
2617
+ this._config.maxElements ?? 5e5,
2618
+ this._config.hnswM,
2619
+ this._config.hnswEfConstruction,
2620
+ this._config.hnswEfSearch
2621
+ );
2622
+ await kvHnsw.init();
2623
+ this._kvService = new KVService(this._db, this._embedding, kvHnsw, /* @__PURE__ */ new Map(), this._config.reranker);
2624
+ if (!skipVectorLoad) {
2625
+ const kvIndexPath = hnswPath(this._config.dbPath, "kv");
2626
+ const kvCount = countRows(this._db, "kv_vectors");
2627
+ if (kvHnsw.tryLoad(kvIndexPath, kvCount)) {
2628
+ loadVecCache(this._db, "kv_vectors", "data_id", this._kvService.vecs);
2629
+ } else {
2630
+ loadVectors(this._db, "kv_vectors", "data_id", kvHnsw, this._kvService.vecs);
2631
+ }
2632
+ }
2633
+ const privateHnsw = /* @__PURE__ */ new Map();
2634
+ for (const mod of this._registry.all) {
2635
+ const pluginDb = this._getOrCreatePluginDb(mod.name);
2636
+ if (pluginDb !== this._db) {
2637
+ setEmbeddingMeta(pluginDb, this._embedding);
2638
+ }
2639
+ const ctx = this._buildPluginContext(skipVectorLoad, privateHnsw, pluginDb, mod.name);
2640
+ await mod.initialize(ctx);
2641
+ }
2642
+ if (this._config.webhookPort) {
2643
+ this._webhookServer = new WebhookServer();
2644
+ this._webhookServer.listen(this._config.webhookPort);
2645
+ }
2646
+ await saveAllHnsw(this._config.dbPath, kvHnsw, this._sharedHnsw, privateHnsw);
2647
+ this._searchAPI = createSearchAPI(
2648
+ this._db,
2649
+ this._embedding,
2650
+ this._config,
2651
+ this._registry,
2652
+ this._kvService,
2653
+ this._sharedHnsw
2654
+ );
2655
+ this._indexDeps = {
2656
+ db: this._db,
2657
+ dbPath: this._config.dbPath,
2658
+ sharedHnsw: this._sharedHnsw,
2659
+ kvHnsw,
2660
+ registry: this._registry,
2661
+ emit: /* @__PURE__ */ __name((e, d) => this.emit(e, d), "emit")
2662
+ };
2663
+ this._loadedVersions = getVersions(this._db);
2664
+ this._initialized = true;
2665
+ this.emit("initialized", { plugins: this.plugins });
2666
+ }
2667
+ /** Reset shared state after a failed `_runInitialize`. */
2668
+ _cleanupAfterFailedInit() {
2669
+ for (const { hnsw } of this._sharedHnsw.values()) {
2670
+ try {
2671
+ hnsw.reinit();
2672
+ } catch (e) {
2673
+ this.emit("warn", `HNSW reinit failed during cleanup: ${e}`);
2674
+ }
2675
+ }
2676
+ this._kvService?.clear();
2677
+ if (this._kvService) {
2678
+ try {
2679
+ this._kvService.hnsw.reinit();
2680
+ } catch (e) {
2681
+ this.emit("warn", `KV HNSW reinit failed during cleanup: ${e}`);
2682
+ }
2683
+ }
2684
+ try {
2685
+ this._db?.close();
2686
+ } catch {
2687
+ }
2688
+ this._db = void 0;
2689
+ this._kvService = void 0;
2690
+ this._searchAPI = void 0;
2691
+ this._indexDeps = void 0;
2692
+ this._initPromise = null;
2693
+ }
2694
+ /** Resolve embedding: explicit config > stored DB key > local default. */
2695
+ async _resolveEmbedding() {
2696
+ if (this._config.embeddingProvider) return this._config.embeddingProvider;
2697
+ const meta = getEmbeddingMeta(this._db);
2698
+ if (meta?.providerKey && meta.providerKey !== "local") {
2699
+ this.emit("progress", `Embedding: auto-resolved '${meta.providerKey}' from DB`);
2700
+ return resolveEmbedding(meta.providerKey);
2701
+ }
2702
+ return resolveEmbedding("local");
2703
+ }
2704
+ /**
2705
+ * Get or create a per-repo SQLiteAdapter for namespaced plugins.
2706
+ * Non-namespaced plugins use the root DB.
2707
+ * DB path: `.brainbank/<repoName>.db` (e.g., `servicehub-backend.db`).
2708
+ */
2709
+ _getOrCreatePluginDb(pluginName) {
2710
+ if (!pluginName.includes(":")) return this._db;
2711
+ const repoName = pluginName.split(":").slice(1).join(":");
2712
+ const existing = this._repoDBs.get(repoName);
2713
+ if (existing) return existing;
2714
+ const dir = path5.dirname(this._config.dbPath);
2715
+ const repoDbPath = path5.join(dir, `${repoName}.db`);
2716
+ const db = new SQLiteAdapter(repoDbPath);
2717
+ this._repoDBs.set(repoName, db);
2718
+ return db;
2719
+ }
2720
+ /** Build a per-plugin `PluginContext` with appropriate DB and HNSW scoping. */
2721
+ _buildPluginContext(skipVectorLoad, privateHnsw, pluginDb, pluginName) {
2722
+ let autoId = 0;
2723
+ const dbPath = this._config.dbPath;
2724
+ return {
2725
+ db: pluginDb,
2726
+ embedding: this._embedding,
2727
+ config: this._config,
2728
+ createHnsw: /* @__PURE__ */ __name(async (maxElements, dims, name) => {
2729
+ const hnsw = await new HNSWIndex(
2730
+ dims ?? this._config.embeddingDims,
2731
+ maxElements ?? this._config.maxElements,
2732
+ this._config.hnswM,
2733
+ this._config.hnswEfConstruction,
2734
+ this._config.hnswEfSearch
2735
+ ).init();
2736
+ privateHnsw.set(name ?? `private-${autoId++}`, hnsw);
2737
+ return hnsw;
2738
+ }, "createHnsw"),
2739
+ loadVectors: /* @__PURE__ */ __name((table, idCol, hnsw, cache) => {
2740
+ if (skipVectorLoad) return;
2741
+ const indexName = table.replace("_vectors", "").replace("_chunks", "");
2742
+ const indexPath = hnswPath(dbPath, `${indexName}-${pluginDb === this._db ? "root" : "repo"}`);
2743
+ const rowCount = countRows(pluginDb, table);
2744
+ if (hnsw.tryLoad(indexPath, rowCount)) {
2745
+ loadVecCache(pluginDb, table, idCol, cache);
2746
+ } else {
2747
+ loadVectors(pluginDb, table, idCol, hnsw, cache);
2748
+ }
2749
+ }, "loadVectors"),
2750
+ getOrCreateSharedHnsw: /* @__PURE__ */ __name(async (type, maxElements, dims) => {
2751
+ const existing = this._sharedHnsw.get(type);
2752
+ if (existing) return { ...existing, isNew: false };
2753
+ const hnsw = await new HNSWIndex(
2754
+ dims ?? this._config.embeddingDims,
2755
+ maxElements ?? this._config.maxElements,
2756
+ this._config.hnswM,
2757
+ this._config.hnswEfConstruction,
2758
+ this._config.hnswEfSearch
2759
+ ).init();
2760
+ const vecCache = /* @__PURE__ */ new Map();
2761
+ this._sharedHnsw.set(type, { hnsw, vecCache });
2762
+ return { hnsw, vecCache, isNew: true };
2763
+ }, "getOrCreateSharedHnsw"),
2764
+ collection: /* @__PURE__ */ __name((name) => this._kvService.collection(name), "collection"),
2765
+ createTracker: /* @__PURE__ */ __name(() => createTracker(pluginDb, pluginName), "createTracker"),
2766
+ webhookServer: this._webhookServer
2767
+ };
2768
+ }
2769
+ /**
2770
+ * Reload a single HNSW index by name.
2771
+ * Discovers the vector table via ReembeddablePlugin capability.
2772
+ * KV is handled directly since it's core-owned.
2773
+ *
2774
+ * The `name` comes from `index_state` and equals the plugin's `mod.name`
2775
+ * (e.g. `code:backend`, `git`, `docs`). This matches the key used in
2776
+ * `getOrCreateSharedHnsw()` during initialization.
2777
+ */
2778
+ _reloadIndex(name) {
2779
+ if (name === HNSW.KV && this._kvService) {
2780
+ reloadHnsw({
2781
+ dbPath: this._config.dbPath,
2782
+ db: this._db,
2783
+ name,
2784
+ hnsw: this._kvService.hnsw,
2785
+ vecCache: this._kvService.vecs,
2786
+ vectorTable: "kv_vectors",
2787
+ idCol: "data_id"
2788
+ });
2789
+ return;
2790
+ }
2791
+ const shared = this._sharedHnsw.get(name);
2792
+ if (!shared) return;
2793
+ for (const mod of this._registry.all) {
2794
+ if (!isReembeddable(mod)) continue;
2795
+ if (mod.name !== name) continue;
2796
+ const cfg = mod.reembedConfig();
2797
+ reloadHnsw({
2798
+ dbPath: this._config.dbPath,
2799
+ db: this._db,
2800
+ name,
2801
+ hnsw: shared.hnsw,
2802
+ vecCache: shared.vecCache,
2803
+ vectorTable: cfg.vectorTable,
2804
+ idCol: cfg.fkColumn
2805
+ });
2806
+ return;
2807
+ }
2808
+ }
2809
+ /** Guard: throw descriptive error if not initialized. */
2810
+ _requireInit(method) {
2811
+ if (!this._initialized) {
2812
+ throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
2813
+ }
2814
+ }
2815
+ };
2816
+
2817
+ // src/cli/utils.ts
2818
+ var c = {
2819
+ green: /* @__PURE__ */ __name((s) => `\x1B[32m${s}\x1B[0m`, "green"),
2820
+ red: /* @__PURE__ */ __name((s) => `\x1B[31m${s}\x1B[0m`, "red"),
2821
+ yellow: /* @__PURE__ */ __name((s) => `\x1B[33m${s}\x1B[0m`, "yellow"),
2822
+ cyan: /* @__PURE__ */ __name((s) => `\x1B[36m${s}\x1B[0m`, "cyan"),
2823
+ dim: /* @__PURE__ */ __name((s) => `\x1B[2m${s}\x1B[0m`, "dim"),
2824
+ bold: /* @__PURE__ */ __name((s) => `\x1B[1m${s}\x1B[0m`, "bold"),
2825
+ magenta: /* @__PURE__ */ __name((s) => `\x1B[35m${s}\x1B[0m`, "magenta")
2826
+ };
2827
+ var args = process.argv.slice(2);
2828
+ function getFlag(name) {
2829
+ const idx = args.indexOf(`--${name}`);
2830
+ return idx >= 0 ? args[idx + 1] : void 0;
2831
+ }
2832
+ __name(getFlag, "getFlag");
2833
+ function hasFlag(name) {
2834
+ return args.includes(`--${name}`);
2835
+ }
2836
+ __name(hasFlag, "hasFlag");
2837
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
2838
+ "repo",
2839
+ "depth",
2840
+ "collection",
2841
+ "pattern",
2842
+ "context",
2843
+ "name",
2844
+ "keep",
2845
+ "reranker",
2846
+ "pruner",
2847
+ "only",
2848
+ "docs",
2849
+ "path",
2850
+ "ignore",
2851
+ "meta",
2852
+ "k",
2853
+ "mode",
2854
+ "limit"
2855
+ ]);
2856
+ function stripFlags(argv) {
2857
+ const result = [];
2858
+ for (let i = 0; i < argv.length; i++) {
2859
+ if (argv[i].startsWith("--")) {
2860
+ const name = argv[i].slice(2);
2861
+ const next = argv[i + 1];
2862
+ if (next !== void 0 && !next.startsWith("--")) {
2863
+ if (VALUE_FLAGS.has(name) || /^\d+$/.test(next)) {
2864
+ i++;
2865
+ }
2866
+ }
2867
+ continue;
2868
+ }
2869
+ result.push(argv[i]);
2870
+ }
2871
+ return result;
2872
+ }
2873
+ __name(stripFlags, "stripFlags");
2874
+ function printResults(results, minScore = 0.7) {
2875
+ const filtered = results.filter((r) => r.score >= minScore).slice(0, 20);
2876
+ if (filtered.length === 0) {
2877
+ console.log(c.yellow(` No results above ${Math.round(minScore * 100)}% score.`));
2878
+ return;
2879
+ }
2880
+ for (const r of filtered) {
2881
+ const score = Math.round(r.score * 100);
2882
+ if (r.type === "code") {
2883
+ const m = r.metadata;
2884
+ console.log(
2885
+ `${c.green(`[CODE ${score}%]`)} ${c.bold(r.filePath)} \u2014 ${m.name || m.chunkType} ${c.dim(`L${m.startLine}-${m.endLine}`)}`
2886
+ );
2887
+ console.log(c.dim(r.content.split("\n").slice(0, 5).join("\n")));
2888
+ console.log("");
2889
+ } else if (r.type === "commit") {
2890
+ const m = r.metadata;
2891
+ console.log(
2892
+ `${c.cyan(`[COMMIT ${score}%]`)} ${c.bold(m.shortHash)} ${r.content} ${c.dim(`(${m.author})`)}`
2893
+ );
2894
+ if (m.files?.length) console.log(c.dim(` Files: ${m.files.slice(0, 4).join(", ")}`));
2895
+ console.log("");
2896
+ } else if (r.type === "document") {
2897
+ const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
2898
+ console.log(
2899
+ `${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.metadata.collection}]${ctx}`
2900
+ );
2901
+ console.log(c.dim(r.content.split("\n").slice(0, 4).join("\n")));
2902
+ console.log("");
2903
+ }
2904
+ }
2905
+ }
2906
+ __name(printResults, "printResults");
2907
+ function findDocsPlugin(brain) {
2908
+ for (const name of brain.plugins) {
2909
+ const p = brain.plugin(name);
2910
+ if (p && isDocsPlugin(p)) return p;
2911
+ }
2912
+ return void 0;
2913
+ }
2914
+ __name(findDocsPlugin, "findDocsPlugin");
2915
+
2916
+ // src/cli/factory/brain-context.ts
2917
+ function contextFromCLI(repoPath) {
2918
+ return {
2919
+ repoPath: repoPath ?? getFlag("repo") ?? ".",
2920
+ env: process.env,
2921
+ flags: {
2922
+ ignore: getFlag("ignore"),
2923
+ reranker: getFlag("reranker"),
2924
+ pruner: getFlag("pruner"),
2925
+ embedding: getFlag("embedding")
2926
+ }
2927
+ };
2928
+ }
2929
+ __name(contextFromCLI, "contextFromCLI");
2930
+ function ctxFlag(ctx, name) {
2931
+ return ctx.flags?.[name];
2932
+ }
2933
+ __name(ctxFlag, "ctxFlag");
2934
+
2935
+ // src/cli/factory/builtin-registration.ts
2936
+ import * as fs5 from "fs";
2937
+ import * as path7 from "path";
2938
+
2939
+ // src/cli/factory/plugin-loader.ts
2940
+ import * as fs4 from "fs";
2941
+ import * as path6 from "path";
2942
+ var PLUGIN_LOADERS = /* @__PURE__ */ new Map([
2943
+ ["code", async () => {
2944
+ try {
2945
+ return (await import("@brainbank/code")).code;
2946
+ } catch {
2947
+ return null;
2948
+ }
2949
+ }],
2950
+ ["git", async () => {
2951
+ try {
2952
+ return (await import("@brainbank/git")).git;
2953
+ } catch {
2954
+ return null;
2955
+ }
2956
+ }],
2957
+ ["docs", async () => {
2958
+ try {
2959
+ return (await import("@brainbank/docs")).docs;
2960
+ } catch {
2961
+ return null;
2962
+ }
2963
+ }]
2964
+ ]);
2965
+ var MULTI_REPO_PLUGINS = /* @__PURE__ */ new Set(["code", "git"]);
2966
+ async function loadPlugin(name) {
2967
+ const loader = PLUGIN_LOADERS.get(name);
2968
+ if (!loader) return null;
2969
+ return loader();
2970
+ }
2971
+ __name(loadPlugin, "loadPlugin");
2972
+ function isMultiRepoCapable(name) {
2973
+ return MULTI_REPO_PLUGINS.has(name);
2974
+ }
2975
+ __name(isMultiRepoCapable, "isMultiRepoCapable");
2976
+ var INDEXER_EXTENSIONS = [".ts", ".js", ".mjs"];
2977
+ var NOT_LOADED = /* @__PURE__ */ Symbol("not-loaded");
2978
+ var _folderPluginsCache = NOT_LOADED;
2979
+ async function discoverFolderPlugins(repoPath) {
2980
+ if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
2981
+ const pluginsDir = path6.resolve(repoPath, ".brainbank", "plugins");
2982
+ if (!fs4.existsSync(pluginsDir)) {
2983
+ _folderPluginsCache = [];
2984
+ return [];
2985
+ }
2986
+ const files = fs4.readdirSync(pluginsDir).filter((f) => INDEXER_EXTENSIONS.some((ext) => f.endsWith(ext))).sort();
2987
+ const plugins = [];
2988
+ for (const file of files) {
2989
+ const filePath = path6.join(pluginsDir, file);
2990
+ try {
2991
+ const mod = await import(filePath);
2992
+ const plugin = mod.default ?? mod;
2993
+ if (plugin && typeof plugin === "object" && plugin.name) {
2994
+ plugins.push(plugin);
2995
+ } else {
2996
+ console.error(c.yellow(`\u26A0 ${file}: must export a default Plugin with a 'name' property, skipping`));
2997
+ }
2998
+ } catch (err) {
2999
+ const message = err instanceof Error ? err.message : String(err);
3000
+ console.error(c.red(`Error loading plugin ${file}: ${message}`));
3001
+ }
3002
+ }
3003
+ _folderPluginsCache = plugins;
3004
+ return plugins;
3005
+ }
3006
+ __name(discoverFolderPlugins, "discoverFolderPlugins");
3007
+ function resetPluginCache() {
3008
+ _folderPluginsCache = NOT_LOADED;
3009
+ }
3010
+ __name(resetPluginCache, "resetPluginCache");
3011
+ async function resolveEmbeddingKey(key) {
3012
+ const { resolveEmbedding: resolveEmbedding2 } = await import("./resolve-Q5D6HECY.js");
3013
+ return resolveEmbedding2(key);
3014
+ }
3015
+ __name(resolveEmbeddingKey, "resolveEmbeddingKey");
3016
+ async function setupProviders(brainOpts, config, flags, env) {
3017
+ const rerankerFlag = flags?.reranker ?? config?.reranker;
3018
+ if (rerankerFlag === "qwen3") {
3019
+ const { Qwen3Reranker } = await import("./qwen3-reranker-HVIQOLKS.js");
3020
+ brainOpts.reranker = new Qwen3Reranker();
3021
+ }
3022
+ const prunerFlag = flags?.pruner ?? config?.pruner;
3023
+ if (prunerFlag === "haiku") {
3024
+ const { HaikuPruner } = await import("./haiku-pruner-DB77ZQLJ.js");
3025
+ brainOpts.pruner = new HaikuPruner();
3026
+ }
3027
+ const expanderFlag = flags?.expander ?? config?.expander;
3028
+ if (expanderFlag === "haiku") {
3029
+ try {
3030
+ const { HaikuExpander } = await import("./haiku-expander-WOVJIVXD.js");
3031
+ brainOpts.expander = new HaikuExpander();
3032
+ } catch {
3033
+ }
3034
+ }
3035
+ const embFlag = flags?.embedding ?? config?.embedding ?? env?.BRAINBANK_EMBEDDING ?? process.env.BRAINBANK_EMBEDDING;
3036
+ if (embFlag) {
3037
+ const provider = await resolveEmbeddingKey(embFlag);
3038
+ brainOpts.embeddingProvider = provider;
3039
+ brainOpts.embeddingDims = provider.dims;
3040
+ }
3041
+ if (config?.context) {
3042
+ brainOpts.contextFields = config.context;
3043
+ }
3044
+ }
3045
+ __name(setupProviders, "setupProviders");
3046
+
3047
+ // src/cli/factory/builtin-registration.ts
3048
+ function pluginCfg(config, pluginName) {
3049
+ const section = config?.[pluginName];
3050
+ if (section && typeof section === "object" && !Array.isArray(section)) {
3051
+ return section;
3052
+ }
3053
+ return {};
3054
+ }
3055
+ __name(pluginCfg, "pluginCfg");
3056
+ function detectGitSubdirs(parentPath, repos) {
3057
+ try {
3058
+ const entries = fs5.readdirSync(parentPath, { withFileTypes: true });
3059
+ let subdirs = entries.filter((e) => {
3060
+ if (e.name.startsWith(".") || e.name.startsWith("node_modules")) return false;
3061
+ const isDir = e.isDirectory() || e.isSymbolicLink() && fs5.statSync(path7.join(parentPath, e.name)).isDirectory();
3062
+ return isDir && fs5.existsSync(path7.join(parentPath, e.name, ".git"));
3063
+ }).map((e) => ({ name: e.name, path: path7.join(parentPath, e.name) }));
3064
+ if (repos && repos.length > 0) {
3065
+ const allowed = new Set(repos);
3066
+ subdirs = subdirs.filter((s) => allowed.has(s.name));
3067
+ }
3068
+ return subdirs;
3069
+ } catch {
3070
+ return [];
3071
+ }
3072
+ }
3073
+ __name(detectGitSubdirs, "detectGitSubdirs");
3074
+ async function registerBuiltins(brain, rp, pluginNames, config, ignorePatterns = []) {
3075
+ const resolvedRp = path7.resolve(rp);
3076
+ const hasRootGit = fs5.existsSync(path7.join(resolvedRp, ".git"));
3077
+ const configRepos = config?.repos;
3078
+ const gitSubdirs = !hasRootGit ? detectGitSubdirs(resolvedRp, configRepos) : [];
3079
+ for (const name of pluginNames) {
3080
+ const factory = await loadPlugin(name);
3081
+ if (!factory) {
3082
+ console.error(c.yellow(` \u26A0 @brainbank/${name} not installed \u2014 skipping ${name} indexing`));
3083
+ console.error(c.dim(` Install: npm i -g @brainbank/${name}`));
3084
+ continue;
3085
+ }
3086
+ const cfg = pluginCfg(config, name);
3087
+ const embKey = cfg.embedding;
3088
+ const embeddingProvider = embKey ? await resolveEmbeddingKey(embKey) : void 0;
3089
+ if (gitSubdirs.length > 0 && isMultiRepoCapable(name)) {
3090
+ console.error(c.cyan(` Multi-repo: found ${gitSubdirs.length} git repos: ${gitSubdirs.map((d) => d.name).join(", ")}`));
3091
+ for (const sub of gitSubdirs) {
3092
+ const mergedIgnore = [...cfg.ignore ?? [], ...ignorePatterns];
3093
+ brain.use(factory({
3094
+ ...cfg,
3095
+ repoPath: sub.path,
3096
+ name: `${name}:${sub.name}`,
3097
+ embeddingProvider,
3098
+ ignore: mergedIgnore.length > 0 ? mergedIgnore : void 0
3099
+ }));
3100
+ }
3101
+ } else {
3102
+ const configIgnore = cfg.ignore ?? [];
3103
+ const mergedIgnore = [...configIgnore, ...ignorePatterns];
3104
+ brain.use(factory({
3105
+ ...cfg,
3106
+ repoPath: rp,
3107
+ embeddingProvider,
3108
+ ignore: mergedIgnore.length > 0 ? mergedIgnore : void 0
3109
+ }));
3110
+ }
3111
+ }
3112
+ }
3113
+ __name(registerBuiltins, "registerBuiltins");
3114
+ async function registerConfigCollections(brain, rp, config) {
3115
+ const docsCfg = pluginCfg(config, "docs");
3116
+ const collections = docsCfg.collections;
3117
+ if (!collections?.length) return;
3118
+ const { isDocsPlugin: isDocsPlugin2 } = await import("./plugin-FF4Q34TI.js");
3119
+ const rawPlugin = brain.plugin("docs");
3120
+ if (!rawPlugin || !isDocsPlugin2(rawPlugin)) return;
3121
+ const repoPath = path7.resolve(rp);
3122
+ for (const coll of collections) {
3123
+ const absPath = path7.resolve(repoPath, coll.path);
3124
+ try {
3125
+ await rawPlugin.addCollection({
3126
+ name: coll.name,
3127
+ path: absPath,
3128
+ pattern: coll.pattern ?? "**/*.md",
3129
+ ignore: coll.ignore,
3130
+ context: coll.context
3131
+ });
3132
+ } catch (e) {
3133
+ if (!(e instanceof Error && e.message.includes("already"))) throw e;
3134
+ }
3135
+ }
3136
+ }
3137
+ __name(registerConfigCollections, "registerConfigCollections");
3138
+
3139
+ // src/cli/factory/config-loader.ts
3140
+ import * as fs6 from "fs";
3141
+ import * as path8 from "path";
3142
+ var CONFIG_NAMES = ["config.json", "config.ts", "config.js", "config.mjs"];
3143
+ var NOT_LOADED2 = /* @__PURE__ */ Symbol("not-loaded");
3144
+ var _configCache = NOT_LOADED2;
3145
+ async function loadConfig(repoPath) {
3146
+ if (_configCache !== NOT_LOADED2) return _configCache;
3147
+ const brainbankDir = path8.resolve(repoPath, ".brainbank");
3148
+ for (const name of CONFIG_NAMES) {
3149
+ const configPath = path8.join(brainbankDir, name);
3150
+ if (!fs6.existsSync(configPath)) continue;
3151
+ try {
3152
+ if (name === "config.json") {
3153
+ const raw = fs6.readFileSync(configPath, "utf-8");
3154
+ _configCache = JSON.parse(raw);
3155
+ } else {
3156
+ const mod = await import(configPath);
3157
+ _configCache = mod.default ?? mod;
3158
+ }
3159
+ return _configCache;
3160
+ } catch (err) {
3161
+ const message = err instanceof Error ? err.message : String(err);
3162
+ console.error(c.red(`Error loading .brainbank/${name}: ${message}`));
3163
+ process.exit(1);
3164
+ }
3165
+ }
3166
+ _configCache = null;
3167
+ return null;
3168
+ }
3169
+ __name(loadConfig, "loadConfig");
3170
+ async function getConfig(repoPath) {
3171
+ return loadConfig(repoPath ?? ".");
3172
+ }
3173
+ __name(getConfig, "getConfig");
3174
+ function resetConfigCache() {
3175
+ _configCache = NOT_LOADED2;
3176
+ }
3177
+ __name(resetConfigCache, "resetConfigCache");
3178
+
3179
+ // src/cli/factory/index.ts
3180
+ function resetFactoryCache() {
3181
+ resetConfigCache();
3182
+ resetPluginCache();
3183
+ }
3184
+ __name(resetFactoryCache, "resetFactoryCache");
3185
+ async function createBrain(contextOrRepo) {
3186
+ const ctx = typeof contextOrRepo === "string" ? contextFromCLI(contextOrRepo) : contextOrRepo ?? contextFromCLI();
3187
+ const rp = ctx.repoPath;
3188
+ const config = await loadConfig(rp);
3189
+ const folderPlugins = await discoverFolderPlugins(rp);
3190
+ const brainOpts = { repoPath: rp, ...config?.brainbank ?? {} };
3191
+ if (config?.maxFileSize) brainOpts.maxFileSize = config.maxFileSize;
3192
+ await setupProviders(brainOpts, config, ctx.flags, ctx.env);
3193
+ const brain = new BrainBank(brainOpts);
3194
+ const builtins = config?.plugins ?? ["code", "git", "docs"];
3195
+ const ignoreFlag = ctxFlag(ctx, "ignore");
3196
+ const ignorePatterns = ignoreFlag ? ignoreFlag.split(",").map((s) => s.trim()) : [];
3197
+ await registerBuiltins(brain, rp, builtins, config, ignorePatterns);
3198
+ for (const plugin of folderPlugins) brain.use(plugin);
3199
+ if (config?.indexers) {
3200
+ for (const plugin of config.indexers) brain.use(plugin);
3201
+ }
3202
+ return brain;
3203
+ }
3204
+ __name(createBrain, "createBrain");
3205
+
3206
+ export {
3207
+ DEFAULTS,
3208
+ resolveConfig,
3209
+ HNSW,
3210
+ SQLiteAdapter,
3211
+ createTracker,
3212
+ bumpVersion,
3213
+ getVersions,
3214
+ getVersion,
3215
+ acquireLock,
3216
+ releaseLock,
3217
+ withLock,
3218
+ cosineSimilarity,
3219
+ normalize,
3220
+ vecToBuffer,
3221
+ rerank,
3222
+ reciprocalRankFusion,
3223
+ pruneResults,
3224
+ ContextBuilder,
3225
+ CompositeBM25Search,
3226
+ CompositeVectorSearch,
3227
+ HNSWIndex,
3228
+ sanitizeFTS,
3229
+ normalizeBM25,
3230
+ escapeLike,
3231
+ Collection,
3232
+ KVService,
3233
+ SUPPORTED_EXTENSIONS,
3234
+ IGNORE_DIRS,
3235
+ isSupported,
3236
+ getLanguage,
3237
+ isIgnoredDir,
3238
+ isIgnoredFile,
3239
+ Watcher,
3240
+ WebhookServer,
3241
+ BrainBank,
3242
+ c,
3243
+ args,
3244
+ getFlag,
3245
+ hasFlag,
3246
+ stripFlags,
3247
+ printResults,
3248
+ findDocsPlugin,
3249
+ contextFromCLI,
3250
+ registerConfigCollections,
3251
+ loadConfig,
3252
+ getConfig,
3253
+ resetFactoryCache,
3254
+ createBrain
3255
+ };
3256
+ //# sourceMappingURL=chunk-JRVYSTMP.js.map