brainbank 0.1.0-beta.1

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