brainbank 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +233 -126
  2. package/dist/{base-DZWtdgIf.d.ts → base-3SNc_CeY.d.ts} +24 -24
  3. package/dist/chunk-424UFCY7.js +78 -0
  4. package/dist/chunk-424UFCY7.js.map +1 -0
  5. package/dist/{chunk-HNPABX7L.js → chunk-7EZR47JV.js} +1 -1
  6. package/dist/{chunk-HNPABX7L.js.map → chunk-7EZR47JV.js.map} +1 -1
  7. package/dist/chunk-B77KABWH.js +41 -0
  8. package/dist/chunk-B77KABWH.js.map +1 -0
  9. package/dist/{chunk-MY36UPPQ.js → chunk-DI3H6JVZ.js} +357 -379
  10. package/dist/chunk-DI3H6JVZ.js.map +1 -0
  11. package/dist/{chunk-DDECTPRM.js → chunk-FGL32LUJ.js} +20 -14
  12. package/dist/chunk-FGL32LUJ.js.map +1 -0
  13. package/dist/{chunk-TTXVJFAE.js → chunk-JRSKWF6K.js} +4 -3
  14. package/dist/{chunk-TTXVJFAE.js.map → chunk-JRSKWF6K.js.map} +1 -1
  15. package/dist/{chunk-YRGUIRN5.js → chunk-VQ27YUHH.js} +18 -14
  16. package/dist/chunk-VQ27YUHH.js.map +1 -0
  17. package/dist/{chunk-BNV43SEF.js → chunk-VVXYZIIB.js} +5 -5
  18. package/dist/chunk-VVXYZIIB.js.map +1 -0
  19. package/dist/chunk-ZNLN2VWV.js +110 -0
  20. package/dist/chunk-ZNLN2VWV.js.map +1 -0
  21. package/dist/cli.js +102 -45
  22. package/dist/cli.js.map +1 -1
  23. package/dist/code.d.ts +4 -2
  24. package/dist/code.js +1 -1
  25. package/dist/docs.d.ts +7 -3
  26. package/dist/docs.js +1 -1
  27. package/dist/git.d.ts +4 -2
  28. package/dist/git.js +1 -1
  29. package/dist/index.d.ts +77 -17
  30. package/dist/index.js +21 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/local-embedding-ZIMTK6PU.js +8 -0
  33. package/dist/local-embedding-ZIMTK6PU.js.map +1 -0
  34. package/dist/memory.d.ts +2 -2
  35. package/dist/memory.js +1 -1
  36. package/dist/notes.d.ts +2 -2
  37. package/dist/notes.js +1 -1
  38. package/dist/qwen3-reranker-3MHEENT5.js +8 -0
  39. package/dist/qwen3-reranker-3MHEENT5.js.map +1 -0
  40. package/dist/resolve-CUJWY6HP.js +10 -0
  41. package/dist/resolve-CUJWY6HP.js.map +1 -0
  42. package/package.json +10 -9
  43. package/dist/chunk-BNV43SEF.js.map +0 -1
  44. package/dist/chunk-DDECTPRM.js.map +0 -1
  45. package/dist/chunk-MY36UPPQ.js.map +0 -1
  46. package/dist/chunk-YRGUIRN5.js.map +0 -1
@@ -1,11 +1,15 @@
1
+ import {
2
+ providerKey,
3
+ resolveEmbedding
4
+ } from "./chunk-B77KABWH.js";
1
5
  import {
2
6
  isIgnoredDir,
3
7
  isIgnoredFile,
4
8
  isSupported
5
- } from "./chunk-DDECTPRM.js";
9
+ } from "./chunk-FGL32LUJ.js";
6
10
  import {
7
11
  rerank
8
- } from "./chunk-YRGUIRN5.js";
12
+ } from "./chunk-VQ27YUHH.js";
9
13
  import {
10
14
  normalizeBM25,
11
15
  reciprocalRankFusion,
@@ -55,7 +59,7 @@ function resolveConfig(partial = {}) {
55
59
  }
56
60
  __name(resolveConfig, "resolveConfig");
57
61
 
58
- // src/core/collection.ts
62
+ // src/domain/collection.ts
59
63
  var Collection = class {
60
64
  constructor(_name, _db, _embedding, _hnsw, _vecs, _reranker) {
61
65
  this._name = _name;
@@ -435,76 +439,6 @@ var HNSWIndex = class {
435
439
  }
436
440
  };
437
441
 
438
- // src/providers/embeddings/local-embedding.ts
439
- var LocalEmbedding = class {
440
- static {
441
- __name(this, "LocalEmbedding");
442
- }
443
- dims = 384;
444
- _pipeline = null;
445
- _modelName;
446
- _cacheDir;
447
- constructor(options = {}) {
448
- this._modelName = options.model ?? "Xenova/all-MiniLM-L6-v2";
449
- this._cacheDir = options.cacheDir ?? ".model-cache";
450
- }
451
- _pipelinePromise = null;
452
- /**
453
- * Lazy-load the transformer pipeline.
454
- * Singleton — created once and reused.
455
- * Promise-deduped to prevent concurrent downloads.
456
- */
457
- async _getPipeline() {
458
- if (this._pipeline) return this._pipeline;
459
- if (this._pipelinePromise) return this._pipelinePromise;
460
- this._pipelinePromise = (async () => {
461
- const { pipeline, env } = await import("@xenova/transformers");
462
- env.cacheDir = this._cacheDir;
463
- env.allowLocalModels = true;
464
- this._pipeline = await pipeline("feature-extraction", this._modelName, {
465
- quantized: true
466
- });
467
- return this._pipeline;
468
- })();
469
- try {
470
- return await this._pipelinePromise;
471
- } finally {
472
- this._pipelinePromise = null;
473
- }
474
- }
475
- /**
476
- * Embed a single text string.
477
- * Returns a normalized Float32Array of length 384.
478
- */
479
- async embed(text) {
480
- const pipe = await this._getPipeline();
481
- const output = await pipe(text, { pooling: "mean", normalize: true });
482
- return output.data;
483
- }
484
- /**
485
- * Embed multiple texts using real batch processing.
486
- * Chunks into groups of BATCH_SIZE to balance throughput vs memory.
487
- */
488
- async embedBatch(texts) {
489
- if (texts.length === 0) return [];
490
- const BATCH_SIZE = 32;
491
- const pipe = await this._getPipeline();
492
- const results = [];
493
- for (let i = 0; i < texts.length; i += BATCH_SIZE) {
494
- const batch = texts.slice(i, i + BATCH_SIZE);
495
- const output = await pipe(batch, { pooling: "mean", normalize: true });
496
- for (let j = 0; j < batch.length; j++) {
497
- const start = j * this.dims;
498
- results.push(output.data.slice(start, start + this.dims));
499
- }
500
- }
501
- return results;
502
- }
503
- async close() {
504
- this._pipeline = null;
505
- }
506
- };
507
-
508
442
  // src/search/vector/mmr.ts
509
443
  function searchMMR(index, query, vectorCache, k, lambda = 0.7) {
510
444
  const candidates = index.search(query, k * 3);
@@ -827,7 +761,7 @@ var KeywordSearch = class {
827
761
  }
828
762
  };
829
763
 
830
- // src/core/context-builder.ts
764
+ // src/search/context-builder.ts
831
765
  var ContextBuilder = class {
832
766
  constructor(_search, _coEdits) {
833
767
  this._search = _search;
@@ -947,21 +881,21 @@ var ContextBuilder = class {
947
881
  // src/brainbank.ts
948
882
  import { EventEmitter } from "events";
949
883
 
950
- // src/core/registry.ts
884
+ // src/bootstrap/registry.ts
951
885
  var ALIASES = {};
952
- var IndexerRegistry = class {
886
+ var PluginRegistry = class {
953
887
  static {
954
- __name(this, "IndexerRegistry");
888
+ __name(this, "PluginRegistry");
955
889
  }
956
890
  _map = /* @__PURE__ */ new Map();
957
891
  // ── Registration ────────────────────────────────
958
- /** Store an indexer. Duplicate names silently overwrite. */
959
- register(indexer) {
960
- this._map.set(indexer.name, indexer);
892
+ /** Store a plugin. Duplicate names silently overwrite. */
893
+ register(plugin) {
894
+ this._map.set(plugin.name, plugin);
961
895
  }
962
896
  // ── Lookup ──────────────────────────────────────
963
897
  /**
964
- * Check whether an indexer is registered.
898
+ * Check whether a plugin is registered.
965
899
  * Supports type-prefix matching: `has('code')` returns true if
966
900
  * 'code', 'code:frontend', or 'code:backend' is registered.
967
901
  */
@@ -973,7 +907,7 @@ var IndexerRegistry = class {
973
907
  return false;
974
908
  }
975
909
  /**
976
- * Get an indexer by name. Throws a descriptive error if not found.
910
+ * Get a plugin by name. Throws a descriptive error if not found.
977
911
  *
978
912
  * Resolution order:
979
913
  * 1. Alias map (currently empty)
@@ -987,11 +921,11 @@ var IndexerRegistry = class {
987
921
  const prefixed = this.firstByType(name);
988
922
  if (prefixed) return prefixed;
989
923
  throw new Error(
990
- `BrainBank: Indexer '${name}' is not loaded. Add .use(${name}()) to your BrainBank instance.`
924
+ `BrainBank: Plugin '${name}' is not loaded. Add .use(${name}()) to your BrainBank instance.`
991
925
  );
992
926
  }
993
927
  /**
994
- * Return every indexer whose name equals `type` or starts with `type + ':'`.
928
+ * Return every plugin whose name equals `type` or starts with `type + ':'`.
995
929
  * Example: allByType('code') → [code, code:frontend, code:backend]
996
930
  */
997
931
  allByType(type) {
@@ -999,7 +933,7 @@ var IndexerRegistry = class {
999
933
  (m) => m.name === type || m.name.startsWith(type + ":")
1000
934
  );
1001
935
  }
1002
- /** Return the first indexer that matches the type prefix, or undefined. */
936
+ /** Return the first plugin that matches the type prefix, or undefined. */
1003
937
  firstByType(type) {
1004
938
  for (const m of this._map.values()) {
1005
939
  if (m.name === type || m.name.startsWith(type + ":")) return m;
@@ -1007,11 +941,11 @@ var IndexerRegistry = class {
1007
941
  return void 0;
1008
942
  }
1009
943
  // ── Accessors ───────────────────────────────────
1010
- /** All registered indexer names (insertion order). */
944
+ /** All registered plugin names (insertion order). */
1011
945
  get names() {
1012
946
  return [...this._map.keys()];
1013
947
  }
1014
- /** All registered indexer instances (insertion order). */
948
+ /** All registered plugin instances (insertion order). */
1015
949
  get all() {
1016
950
  return [...this._map.values()];
1017
951
  }
@@ -1023,13 +957,13 @@ var IndexerRegistry = class {
1023
957
  return this._map;
1024
958
  }
1025
959
  // ── Lifecycle ───────────────────────────────────
1026
- /** Remove all registered indexers. Called by BrainBank.close(). */
960
+ /** Remove all registered plugins. Called by BrainBank.close(). */
1027
961
  clear() {
1028
962
  this._map.clear();
1029
963
  }
1030
964
  };
1031
965
 
1032
- // src/core/initializer.ts
966
+ // src/bootstrap/initializer.ts
1033
967
  import { dirname as dirname2, join as join2 } from "path";
1034
968
 
1035
969
  // src/db/database.ts
@@ -1401,165 +1335,7 @@ var Database = class {
1401
1335
  }
1402
1336
  };
1403
1337
 
1404
- // src/services/reembed.ts
1405
- var TABLES = [
1406
- {
1407
- name: "code",
1408
- textTable: "code_chunks",
1409
- vectorTable: "code_vectors",
1410
- idColumn: "id",
1411
- fkColumn: "chunk_id",
1412
- textBuilder: /* @__PURE__ */ __name((r) => [
1413
- `File: ${r.file_path}`,
1414
- r.name ? `${r.chunk_type}: ${r.name}` : r.chunk_type,
1415
- r.content
1416
- ].join("\n"), "textBuilder")
1417
- },
1418
- {
1419
- name: "git",
1420
- textTable: "git_commits",
1421
- vectorTable: "git_vectors",
1422
- idColumn: "id",
1423
- fkColumn: "commit_id",
1424
- // Must match git-engine.ts:119-125 exactly
1425
- textBuilder: /* @__PURE__ */ __name((r) => [
1426
- `Commit: ${r.message}`,
1427
- `Author: ${r.author}`,
1428
- `Date: ${r.date}`,
1429
- r.files_json && r.files_json !== "[]" ? `Files: ${JSON.parse(r.files_json).join(", ")}` : "",
1430
- r.diff ? `Changes:
1431
- ${r.diff.slice(0, 2e3)}` : ""
1432
- ].filter(Boolean).join("\n"), "textBuilder")
1433
- },
1434
- {
1435
- name: "memory",
1436
- textTable: "memory_patterns",
1437
- vectorTable: "memory_vectors",
1438
- idColumn: "id",
1439
- fkColumn: "pattern_id",
1440
- // Must match memory/pattern-store.ts:49 exactly
1441
- textBuilder: /* @__PURE__ */ __name((r) => `${r.task_type} ${r.task} ${r.approach}`, "textBuilder")
1442
- },
1443
- {
1444
- name: "notes",
1445
- textTable: "note_memories",
1446
- vectorTable: "note_vectors",
1447
- idColumn: "id",
1448
- fkColumn: "note_id",
1449
- // Must match notes/engine.ts:90 exactly
1450
- textBuilder: /* @__PURE__ */ __name((r) => {
1451
- const decisions = JSON.parse(r.decisions_json || "[]").join(". ");
1452
- const patterns = JSON.parse(r.patterns_json || "[]").join(". ");
1453
- return `${r.title}
1454
- ${r.summary}
1455
- ${decisions}
1456
- ${patterns}`;
1457
- }, "textBuilder")
1458
- },
1459
- {
1460
- name: "docs",
1461
- textTable: "doc_chunks",
1462
- vectorTable: "doc_vectors",
1463
- idColumn: "id",
1464
- fkColumn: "chunk_id",
1465
- // Must match docs-engine.ts:160 exactly
1466
- textBuilder: /* @__PURE__ */ __name((r) => `title: ${r.title ?? ""} | text: ${r.content}`, "textBuilder")
1467
- },
1468
- {
1469
- name: "kv",
1470
- textTable: "kv_data",
1471
- vectorTable: "kv_vectors",
1472
- idColumn: "id",
1473
- fkColumn: "data_id",
1474
- textBuilder: /* @__PURE__ */ __name((r) => r.content, "textBuilder")
1475
- }
1476
- ];
1477
- async function reembedAll(db, embedding, hnswMap, options = {}) {
1478
- const { batchSize = 50, onProgress } = options;
1479
- const result = {};
1480
- let total = 0;
1481
- for (const table of TABLES) {
1482
- const count = await reembedTable(db, embedding, table, batchSize, onProgress);
1483
- result[table.name] = count;
1484
- total += count;
1485
- const entry = hnswMap.get(table.name);
1486
- if (entry && count > 0) {
1487
- await rebuildHnsw(db, table, entry.hnsw, entry.vecs);
1488
- }
1489
- }
1490
- const meta = {
1491
- provider: embedding.constructor?.name ?? "unknown",
1492
- dims: String(embedding.dims),
1493
- reembedded_at: (/* @__PURE__ */ new Date()).toISOString()
1494
- };
1495
- const upsert = db.prepare(
1496
- "INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)"
1497
- );
1498
- for (const [k, v] of Object.entries(meta)) {
1499
- upsert.run(k, v);
1500
- }
1501
- return {
1502
- code: result.code ?? 0,
1503
- git: result.git ?? 0,
1504
- memory: result.memory ?? 0,
1505
- notes: result.notes ?? 0,
1506
- docs: result.docs ?? 0,
1507
- kv: result.kv ?? 0,
1508
- total
1509
- };
1510
- }
1511
- __name(reembedAll, "reembedAll");
1512
- async function reembedTable(db, embedding, table, batchSize, onProgress) {
1513
- const totalCount = db.prepare(
1514
- `SELECT COUNT(*) as c FROM ${table.textTable}`
1515
- ).get().c;
1516
- if (totalCount === 0) return 0;
1517
- const tempTable = `_reembed_${table.vectorTable}`;
1518
- db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
1519
- db.exec(`CREATE TABLE ${tempTable} AS SELECT * FROM ${table.vectorTable} WHERE 0`);
1520
- const insertTemp = db.prepare(
1521
- `INSERT INTO ${tempTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
1522
- );
1523
- let processed = 0;
1524
- try {
1525
- for (let offset = 0; offset < totalCount; offset += batchSize) {
1526
- const batch = db.prepare(
1527
- `SELECT * FROM ${table.textTable} LIMIT ? OFFSET ?`
1528
- ).all(batchSize, offset);
1529
- const texts = batch.map((r) => table.textBuilder(r));
1530
- const vectors = await embedding.embedBatch(texts);
1531
- db.transaction(() => {
1532
- for (let j = 0; j < batch.length; j++) {
1533
- insertTemp.run(batch[j][table.idColumn], vecToBuffer(vectors[j]));
1534
- }
1535
- });
1536
- processed += batch.length;
1537
- onProgress?.(table.name, processed, totalCount);
1538
- }
1539
- db.transaction(() => {
1540
- db.exec(`DELETE FROM ${table.vectorTable}`);
1541
- db.exec(`INSERT INTO ${table.vectorTable} SELECT * FROM ${tempTable}`);
1542
- });
1543
- } finally {
1544
- db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
1545
- }
1546
- return processed;
1547
- }
1548
- __name(reembedTable, "reembedTable");
1549
- async function rebuildHnsw(db, table, hnsw, vecs) {
1550
- vecs.clear();
1551
- hnsw.reinit();
1552
- const rows = db.prepare(
1553
- `SELECT ${table.fkColumn} as id, embedding FROM ${table.vectorTable}`
1554
- ).all();
1555
- for (const row of rows) {
1556
- const buf = Buffer.from(row.embedding);
1557
- const vec = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
1558
- hnsw.add(vec, row.id);
1559
- vecs.set(row.id, vec);
1560
- }
1561
- }
1562
- __name(rebuildHnsw, "rebuildHnsw");
1338
+ // src/services/embedding-meta.ts
1563
1339
  function getEmbeddingMeta(db) {
1564
1340
  try {
1565
1341
  const provider = db.prepare(
@@ -1568,8 +1344,15 @@ function getEmbeddingMeta(db) {
1568
1344
  const dims = db.prepare(
1569
1345
  "SELECT value FROM embedding_meta WHERE key = 'dims'"
1570
1346
  ).get();
1347
+ const key = db.prepare(
1348
+ "SELECT value FROM embedding_meta WHERE key = 'provider_key'"
1349
+ ).get();
1571
1350
  if (!provider || !dims) return null;
1572
- return { provider: provider.value, dims: Number(dims.value) };
1351
+ return {
1352
+ provider: provider.value,
1353
+ dims: Number(dims.value),
1354
+ providerKey: key?.value ?? "local"
1355
+ };
1573
1356
  } catch {
1574
1357
  return null;
1575
1358
  }
@@ -1581,6 +1364,7 @@ function setEmbeddingMeta(db, embedding) {
1581
1364
  );
1582
1365
  upsert.run("provider", embedding.constructor?.name ?? "unknown");
1583
1366
  upsert.run("dims", String(embedding.dims));
1367
+ upsert.run("provider_key", providerKey(embedding));
1584
1368
  upsert.run("indexed_at", (/* @__PURE__ */ new Date()).toISOString());
1585
1369
  }
1586
1370
  __name(setEmbeddingMeta, "setEmbeddingMeta");
@@ -1597,112 +1381,138 @@ function detectProviderMismatch(db, embedding) {
1597
1381
  }
1598
1382
  __name(detectProviderMismatch, "detectProviderMismatch");
1599
1383
 
1600
- // src/core/initializer.ts
1601
- async function earlyInit(config, emit, options = {}) {
1602
- const db = new Database(config.dbPath);
1603
- const embedding = config.embeddingProvider ?? new LocalEmbedding();
1604
- const mismatch = detectProviderMismatch(db, embedding);
1605
- if (mismatch?.mismatch && !options.force) {
1606
- db.close();
1607
- throw new Error(
1608
- `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.`
1609
- );
1384
+ // src/bootstrap/initializer.ts
1385
+ var Initializer = class {
1386
+ static {
1387
+ __name(this, "Initializer");
1610
1388
  }
1611
- setEmbeddingMeta(db, embedding);
1612
- const kvHnsw = new HNSWIndex(
1613
- config.embeddingDims,
1614
- config.maxElements ?? 5e5,
1615
- config.hnswM,
1616
- config.hnswEfConstruction,
1617
- config.hnswEfSearch
1618
- );
1619
- await kvHnsw.init();
1620
- const skipVectorLoad = !!(options.force && mismatch?.mismatch);
1621
- return { db, embedding, kvHnsw, skipVectorLoad };
1622
- }
1623
- __name(earlyInit, "earlyInit");
1624
- async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollection) {
1625
- const { db, embedding, kvHnsw, skipVectorLoad } = early;
1626
- if (!skipVectorLoad) {
1627
- const kvIndexPath = hnswPath(config.dbPath, "kv");
1628
- const kvCount = countRows(db, "kv_vectors");
1629
- if (kvHnsw.tryLoad(kvIndexPath, kvCount)) {
1630
- loadVecCache(db, "kv_vectors", "data_id", kvVecs);
1631
- } else {
1632
- loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
1389
+ _config;
1390
+ _emit;
1391
+ constructor(config, emit) {
1392
+ this._config = config;
1393
+ this._emit = emit;
1394
+ }
1395
+ /** Phase 1: database, embedding provider, KV HNSW index. */
1396
+ async early(options = {}) {
1397
+ const { _config: config } = this;
1398
+ const db = new Database(config.dbPath);
1399
+ const embedding = await this._resolveEmbedding(db);
1400
+ const mismatch = detectProviderMismatch(db, embedding);
1401
+ if (mismatch?.mismatch && !options.force) {
1402
+ db.close();
1403
+ throw new Error(
1404
+ `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.`
1405
+ );
1633
1406
  }
1634
- }
1635
- const ctx = buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection);
1636
- for (const mod of registry.all) {
1637
- await mod.initialize(ctx);
1638
- }
1639
- saveAllHnsw(config.dbPath, kvHnsw, sharedHnsw);
1640
- return buildSearchLayer(db, embedding, config, registry, sharedHnsw);
1641
- }
1642
- __name(lateInit, "lateInit");
1643
- function buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection) {
1644
- return {
1645
- db,
1646
- embedding,
1647
- config,
1648
- createHnsw: /* @__PURE__ */ __name((maxElements) => new HNSWIndex(
1407
+ setEmbeddingMeta(db, embedding);
1408
+ const kvHnsw = new HNSWIndex(
1649
1409
  config.embeddingDims,
1650
- maxElements ?? config.maxElements,
1410
+ config.maxElements ?? 5e5,
1651
1411
  config.hnswM,
1652
1412
  config.hnswEfConstruction,
1653
1413
  config.hnswEfSearch
1654
- ).init(), "createHnsw"),
1655
- loadVectors: /* @__PURE__ */ __name((table, idCol, hnsw, cache) => {
1656
- if (skipVectorLoad) return;
1657
- const indexName = table.replace("_vectors", "").replace("_chunks", "");
1658
- const indexPath = hnswPath(config.dbPath, indexName);
1659
- const rowCount = countRows(db, table);
1660
- if (hnsw.tryLoad(indexPath, rowCount)) {
1661
- loadVecCache(db, table, idCol, cache);
1414
+ );
1415
+ await kvHnsw.init();
1416
+ const skipVectorLoad = !!(options.force && mismatch?.mismatch);
1417
+ return { db, embedding, kvHnsw, skipVectorLoad };
1418
+ }
1419
+ /** Phase 2: load vectors, run indexers, build search layer. */
1420
+ async late(earlyResult, registry, sharedHnsw, kvVecs, getCollection) {
1421
+ const { _config: config } = this;
1422
+ const { db, embedding, kvHnsw, skipVectorLoad } = earlyResult;
1423
+ if (!skipVectorLoad) {
1424
+ const kvIndexPath = hnswPath(config.dbPath, "kv");
1425
+ const kvCount = countRows(db, "kv_vectors");
1426
+ if (kvHnsw.tryLoad(kvIndexPath, kvCount)) {
1427
+ loadVecCache(db, "kv_vectors", "data_id", kvVecs);
1662
1428
  } else {
1663
- loadVectors(db, table, idCol, hnsw, cache);
1429
+ loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
1664
1430
  }
1665
- }, "loadVectors"),
1666
- getOrCreateSharedHnsw: /* @__PURE__ */ __name(async (type, maxElements) => {
1667
- const existing = sharedHnsw.get(type);
1668
- if (existing) return { ...existing, isNew: false };
1669
- const hnsw = await new HNSWIndex(
1670
- config.embeddingDims,
1431
+ }
1432
+ const ctx = this._buildPluginContext(db, embedding, sharedHnsw, skipVectorLoad, getCollection);
1433
+ for (const mod of registry.all) {
1434
+ await mod.initialize(ctx);
1435
+ }
1436
+ saveAllHnsw(config.dbPath, kvHnsw, sharedHnsw);
1437
+ return this._buildSearchLayer(db, embedding, registry, sharedHnsw);
1438
+ }
1439
+ /** Build the PluginContext passed to each plugin's initialize(). */
1440
+ _buildPluginContext(db, embedding, sharedHnsw, skipVectorLoad, getCollection) {
1441
+ const { _config: config } = this;
1442
+ return {
1443
+ db,
1444
+ embedding,
1445
+ config,
1446
+ createHnsw: /* @__PURE__ */ __name((maxElements, dims) => new HNSWIndex(
1447
+ dims ?? config.embeddingDims,
1671
1448
  maxElements ?? config.maxElements,
1672
1449
  config.hnswM,
1673
1450
  config.hnswEfConstruction,
1674
1451
  config.hnswEfSearch
1675
- ).init();
1676
- const vecCache = /* @__PURE__ */ new Map();
1677
- sharedHnsw.set(type, { hnsw, vecCache });
1678
- return { hnsw, vecCache, isNew: true };
1679
- }, "getOrCreateSharedHnsw"),
1680
- collection: getCollection
1681
- };
1682
- }
1683
- __name(buildIndexerContext, "buildIndexerContext");
1684
- function buildSearchLayer(db, embedding, config, registry, sharedHnsw) {
1685
- const codeMod = sharedHnsw.get("code");
1686
- const gitMod = sharedHnsw.get("git");
1687
- const memMod = registry.firstByType("memory");
1688
- if (!codeMod && !gitMod && !memMod) return {};
1689
- const search = new VectorSearch({
1690
- db,
1691
- codeHnsw: codeMod?.hnsw,
1692
- gitHnsw: gitMod?.hnsw,
1693
- patternHnsw: memMod?.hnsw,
1694
- codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
1695
- gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
1696
- patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
1697
- embedding,
1698
- reranker: config.reranker
1699
- });
1700
- const bm25 = new KeywordSearch(db);
1701
- const firstGit = registry.firstByType("git");
1702
- const contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
1703
- return { search, bm25, contextBuilder };
1704
- }
1705
- __name(buildSearchLayer, "buildSearchLayer");
1452
+ ).init(), "createHnsw"),
1453
+ loadVectors: /* @__PURE__ */ __name((table, idCol, hnsw, cache) => {
1454
+ if (skipVectorLoad) return;
1455
+ const indexName = table.replace("_vectors", "").replace("_chunks", "");
1456
+ const indexPath = hnswPath(config.dbPath, indexName);
1457
+ const rowCount = countRows(db, table);
1458
+ if (hnsw.tryLoad(indexPath, rowCount)) {
1459
+ loadVecCache(db, table, idCol, cache);
1460
+ } else {
1461
+ loadVectors(db, table, idCol, hnsw, cache);
1462
+ }
1463
+ }, "loadVectors"),
1464
+ getOrCreateSharedHnsw: /* @__PURE__ */ __name(async (type, maxElements, dims) => {
1465
+ const existing = sharedHnsw.get(type);
1466
+ if (existing) return { ...existing, isNew: false };
1467
+ const hnswDims = dims ?? config.embeddingDims;
1468
+ const hnsw = await new HNSWIndex(
1469
+ hnswDims,
1470
+ maxElements ?? config.maxElements,
1471
+ config.hnswM,
1472
+ config.hnswEfConstruction,
1473
+ config.hnswEfSearch
1474
+ ).init();
1475
+ const vecCache = /* @__PURE__ */ new Map();
1476
+ sharedHnsw.set(type, { hnsw, vecCache });
1477
+ return { hnsw, vecCache, isNew: true };
1478
+ }, "getOrCreateSharedHnsw"),
1479
+ collection: getCollection
1480
+ };
1481
+ }
1482
+ /** Build VectorSearch + KeywordSearch + ContextBuilder from initialized plugins. */
1483
+ _buildSearchLayer(db, embedding, registry, sharedHnsw) {
1484
+ const { _config: config } = this;
1485
+ const codeMod = sharedHnsw.get("code");
1486
+ const gitMod = sharedHnsw.get("git");
1487
+ const memMod = registry.firstByType("memory");
1488
+ if (!codeMod && !gitMod && !memMod) return {};
1489
+ const search = new VectorSearch({
1490
+ db,
1491
+ codeHnsw: codeMod?.hnsw,
1492
+ gitHnsw: gitMod?.hnsw,
1493
+ patternHnsw: memMod?.hnsw,
1494
+ codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
1495
+ gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
1496
+ patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
1497
+ embedding,
1498
+ reranker: config.reranker
1499
+ });
1500
+ const bm25 = new KeywordSearch(db);
1501
+ const firstGit = registry.firstByType("git");
1502
+ const contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
1503
+ return { search, bm25, contextBuilder };
1504
+ }
1505
+ /** Resolve embedding: explicit config > stored DB key > local default. */
1506
+ async _resolveEmbedding(db) {
1507
+ if (this._config.embeddingProvider) return this._config.embeddingProvider;
1508
+ const meta = getEmbeddingMeta(db);
1509
+ if (meta?.providerKey && meta.providerKey !== "local") {
1510
+ this._emit("progress", `Embedding: auto-resolved '${meta.providerKey}' from DB`);
1511
+ return resolveEmbedding(meta.providerKey);
1512
+ }
1513
+ return resolveEmbedding("local");
1514
+ }
1515
+ };
1706
1516
  function hnswPath(dbPath, name) {
1707
1517
  return join2(dirname2(dbPath), `hnsw-${name}.index`);
1708
1518
  }
@@ -1750,7 +1560,7 @@ function loadVecCache(db, table, idCol, cache) {
1750
1560
  }
1751
1561
  __name(loadVecCache, "loadVecCache");
1752
1562
 
1753
- // src/core/search-api.ts
1563
+ // src/api/search-api.ts
1754
1564
  var SearchAPI = class {
1755
1565
  constructor(_d) {
1756
1566
  this._d = _d;
@@ -1761,7 +1571,7 @@ var SearchAPI = class {
1761
1571
  // ── Vector ──────────────────────────────────────
1762
1572
  async search(query, options) {
1763
1573
  if (!this._d.search) {
1764
- return this._d.registry.has("docs") ? this._d.searchDocs(query, { k: 8 }) : [];
1574
+ return this._d.registry.has("docs") ? await this._searchDocs(query, { k: 8 }) : [];
1765
1575
  }
1766
1576
  return this._d.search.search(query, options);
1767
1577
  }
@@ -1789,18 +1599,18 @@ var SearchAPI = class {
1789
1599
  if (this._d.search) {
1790
1600
  const [vec, kw] = await Promise.all([
1791
1601
  this._d.search.search(query, { ...options, codeK, gitK }),
1792
- Promise.resolve(this._d.bm25.search(query, { codeK, gitK }))
1602
+ Promise.resolve(this._d.bm25?.search(query, { codeK, gitK }) ?? [])
1793
1603
  ]);
1794
1604
  resultLists.push(vec, kw);
1795
1605
  }
1796
1606
  if (this._d.registry.has("docs")) {
1797
- const docs = await this._d.searchDocs(query, { k: docsK });
1607
+ const docs = await this._searchDocs(query, { k: docsK });
1798
1608
  if (docs.length > 0) resultLists.push(docs);
1799
1609
  }
1800
1610
  await this._searchKvCollections(query, cols, resultLists);
1801
1611
  if (resultLists.length === 0) return [];
1802
1612
  const fused = reciprocalRankFusion(resultLists);
1803
- return this._applyReranking(query, fused);
1613
+ return this._rerankResults(query, fused);
1804
1614
  }
1805
1615
  /** Search non-reserved KV collections and push results. */
1806
1616
  async _searchKvCollections(query, cols, resultLists) {
@@ -1819,10 +1629,16 @@ var SearchAPI = class {
1819
1629
  }
1820
1630
  }
1821
1631
  /** Apply reranking if a reranker is configured. */
1822
- async _applyReranking(query, fused) {
1632
+ async _rerankResults(query, fused) {
1823
1633
  if (!this._d.config.reranker || fused.length <= 1) return fused;
1824
1634
  return rerank(query, fused, this._d.config.reranker);
1825
1635
  }
1636
+ /** Search docs directly via the plugin — no circular callback. */
1637
+ async _searchDocs(query, options) {
1638
+ const plugin = this._d.getDocsPlugin();
1639
+ if (!plugin) return [];
1640
+ return plugin.search(query, options);
1641
+ }
1826
1642
  // ── Keyword ─────────────────────────────────────
1827
1643
  async searchBM25(query, options) {
1828
1644
  return this._d.bm25?.search(query, options) ?? [];
@@ -1838,7 +1654,7 @@ var SearchAPI = class {
1838
1654
  if (core) sections.push(core);
1839
1655
  }
1840
1656
  if (this._d.registry.has("docs")) {
1841
- const docs = await this._d.searchDocs(task, { k: options.codeResults ?? 4 });
1657
+ const docs = await this._searchDocs(task, { k: options.codeResults ?? 4 });
1842
1658
  if (docs.length > 0) {
1843
1659
  const body = docs.map((r) => {
1844
1660
  const m = r.metadata;
@@ -1870,7 +1686,7 @@ function isCollectionPlugin(i) {
1870
1686
  }
1871
1687
  __name(isCollectionPlugin, "isCollectionPlugin");
1872
1688
 
1873
- // src/core/index-api.ts
1689
+ // src/api/index-api.ts
1874
1690
  var IndexAPI = class {
1875
1691
  constructor(_d) {
1876
1692
  this._d = _d;
@@ -1953,6 +1769,166 @@ var IndexAPI = class {
1953
1769
  }
1954
1770
  };
1955
1771
 
1772
+ // src/services/reembed.ts
1773
+ var TABLES = [
1774
+ {
1775
+ name: "code",
1776
+ textTable: "code_chunks",
1777
+ vectorTable: "code_vectors",
1778
+ idColumn: "id",
1779
+ fkColumn: "chunk_id",
1780
+ textBuilder: /* @__PURE__ */ __name((r) => [
1781
+ `File: ${r.file_path}`,
1782
+ r.name ? `${r.chunk_type}: ${r.name}` : r.chunk_type,
1783
+ r.content
1784
+ ].join("\n"), "textBuilder")
1785
+ },
1786
+ {
1787
+ name: "git",
1788
+ textTable: "git_commits",
1789
+ vectorTable: "git_vectors",
1790
+ idColumn: "id",
1791
+ fkColumn: "commit_id",
1792
+ // Must match git-engine.ts:119-125 exactly
1793
+ textBuilder: /* @__PURE__ */ __name((r) => [
1794
+ `Commit: ${r.message}`,
1795
+ `Author: ${r.author}`,
1796
+ `Date: ${r.date}`,
1797
+ r.files_json && r.files_json !== "[]" ? `Files: ${JSON.parse(r.files_json).join(", ")}` : "",
1798
+ r.diff ? `Changes:
1799
+ ${r.diff.slice(0, 2e3)}` : ""
1800
+ ].filter(Boolean).join("\n"), "textBuilder")
1801
+ },
1802
+ {
1803
+ name: "memory",
1804
+ textTable: "memory_patterns",
1805
+ vectorTable: "memory_vectors",
1806
+ idColumn: "id",
1807
+ fkColumn: "pattern_id",
1808
+ // Must match memory/pattern-store.ts:49 exactly
1809
+ textBuilder: /* @__PURE__ */ __name((r) => `${r.task_type} ${r.task} ${r.approach}`, "textBuilder")
1810
+ },
1811
+ {
1812
+ name: "notes",
1813
+ textTable: "note_memories",
1814
+ vectorTable: "note_vectors",
1815
+ idColumn: "id",
1816
+ fkColumn: "note_id",
1817
+ // Must match notes/engine.ts:90 exactly
1818
+ textBuilder: /* @__PURE__ */ __name((r) => {
1819
+ const decisions = JSON.parse(r.decisions_json || "[]").join(". ");
1820
+ const patterns = JSON.parse(r.patterns_json || "[]").join(". ");
1821
+ return `${r.title}
1822
+ ${r.summary}
1823
+ ${decisions}
1824
+ ${patterns}`;
1825
+ }, "textBuilder")
1826
+ },
1827
+ {
1828
+ name: "docs",
1829
+ textTable: "doc_chunks",
1830
+ vectorTable: "doc_vectors",
1831
+ idColumn: "id",
1832
+ fkColumn: "chunk_id",
1833
+ // Must match docs-engine.ts:160 exactly
1834
+ textBuilder: /* @__PURE__ */ __name((r) => `title: ${r.title ?? ""} | text: ${r.content}`, "textBuilder")
1835
+ },
1836
+ {
1837
+ name: "kv",
1838
+ textTable: "kv_data",
1839
+ vectorTable: "kv_vectors",
1840
+ idColumn: "id",
1841
+ fkColumn: "data_id",
1842
+ textBuilder: /* @__PURE__ */ __name((r) => r.content, "textBuilder")
1843
+ }
1844
+ ];
1845
+ async function reembedAll(db, embedding, hnswMap, options = {}) {
1846
+ const { batchSize = 50, onProgress } = options;
1847
+ const result = {};
1848
+ let total = 0;
1849
+ for (const table of TABLES) {
1850
+ const count = await reembedTable(db, embedding, table, batchSize, onProgress);
1851
+ result[table.name] = count;
1852
+ total += count;
1853
+ const entry = hnswMap.get(table.name);
1854
+ if (entry && count > 0) {
1855
+ await rebuildHnsw(db, table, entry.hnsw, entry.vecs);
1856
+ }
1857
+ }
1858
+ const meta = {
1859
+ provider: embedding.constructor?.name ?? "unknown",
1860
+ dims: String(embedding.dims),
1861
+ reembedded_at: (/* @__PURE__ */ new Date()).toISOString()
1862
+ };
1863
+ const upsert = db.prepare(
1864
+ "INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)"
1865
+ );
1866
+ for (const [k, v] of Object.entries(meta)) {
1867
+ upsert.run(k, v);
1868
+ }
1869
+ return {
1870
+ code: result.code ?? 0,
1871
+ git: result.git ?? 0,
1872
+ memory: result.memory ?? 0,
1873
+ notes: result.notes ?? 0,
1874
+ docs: result.docs ?? 0,
1875
+ kv: result.kv ?? 0,
1876
+ total
1877
+ };
1878
+ }
1879
+ __name(reembedAll, "reembedAll");
1880
+ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1881
+ const totalCount = db.prepare(
1882
+ `SELECT COUNT(*) as c FROM ${table.textTable}`
1883
+ ).get().c;
1884
+ if (totalCount === 0) return 0;
1885
+ const tempTable = `_reembed_${table.vectorTable}`;
1886
+ db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
1887
+ db.exec(`CREATE TABLE ${tempTable} AS SELECT * FROM ${table.vectorTable} WHERE 0`);
1888
+ const insertTemp = db.prepare(
1889
+ `INSERT INTO ${tempTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
1890
+ );
1891
+ let processed = 0;
1892
+ try {
1893
+ for (let offset = 0; offset < totalCount; offset += batchSize) {
1894
+ const batch = db.prepare(
1895
+ `SELECT * FROM ${table.textTable} LIMIT ? OFFSET ?`
1896
+ ).all(batchSize, offset);
1897
+ const texts = batch.map((r) => table.textBuilder(r));
1898
+ const vectors = await embedding.embedBatch(texts);
1899
+ db.transaction(() => {
1900
+ for (let j = 0; j < batch.length; j++) {
1901
+ insertTemp.run(batch[j][table.idColumn], vecToBuffer(vectors[j]));
1902
+ }
1903
+ });
1904
+ processed += batch.length;
1905
+ onProgress?.(table.name, processed, totalCount);
1906
+ }
1907
+ db.transaction(() => {
1908
+ db.exec(`DELETE FROM ${table.vectorTable}`);
1909
+ db.exec(`INSERT INTO ${table.vectorTable} SELECT * FROM ${tempTable}`);
1910
+ });
1911
+ } finally {
1912
+ db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
1913
+ }
1914
+ return processed;
1915
+ }
1916
+ __name(reembedTable, "reembedTable");
1917
+ async function rebuildHnsw(db, table, hnsw, vecs) {
1918
+ vecs.clear();
1919
+ hnsw.reinit();
1920
+ const rows = db.prepare(
1921
+ `SELECT ${table.fkColumn} as id, embedding FROM ${table.vectorTable}`
1922
+ ).all();
1923
+ for (const row of rows) {
1924
+ const buf = Buffer.from(row.embedding);
1925
+ const vec = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
1926
+ hnsw.add(vec, row.id);
1927
+ vecs.set(row.id, vec);
1928
+ }
1929
+ }
1930
+ __name(rebuildHnsw, "rebuildHnsw");
1931
+
1956
1932
  // src/services/watch.ts
1957
1933
  import * as fs2 from "fs";
1958
1934
  import * as path3 from "path";
@@ -1973,7 +1949,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1973
1949
  customPatterns.push({ indexer, patterns: indexer.watchPatterns() });
1974
1950
  }
1975
1951
  }
1976
- function matchCustomIndexer(filePath) {
1952
+ function matchCustomPlugin(filePath) {
1977
1953
  const rel = path3.relative(repoPath, filePath);
1978
1954
  for (const { indexer, patterns } of customPatterns) {
1979
1955
  for (const pattern of patterns) {
@@ -1982,7 +1958,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1982
1958
  }
1983
1959
  return null;
1984
1960
  }
1985
- __name(matchCustomIndexer, "matchCustomIndexer");
1961
+ __name(matchCustomPlugin, "matchCustomPlugin");
1986
1962
  function matchGlob(filePath, pattern) {
1987
1963
  if (pattern.startsWith("**/")) {
1988
1964
  const suffix = pattern.slice(3);
@@ -1997,7 +1973,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1997
1973
  }
1998
1974
  __name(matchGlob, "matchGlob");
1999
1975
  let flushing = false;
2000
- async function flush() {
1976
+ async function processPending() {
2001
1977
  if (flushing || pending.size === 0) return;
2002
1978
  flushing = true;
2003
1979
  try {
@@ -2006,7 +1982,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
2006
1982
  let needsReindex = false;
2007
1983
  for (const filePath of files) {
2008
1984
  const absPath = path3.resolve(repoPath, filePath);
2009
- const customIndexer = matchCustomIndexer(absPath);
1985
+ const customIndexer = matchCustomPlugin(absPath);
2010
1986
  if (customIndexer && isWatchable(customIndexer)) {
2011
1987
  try {
2012
1988
  const handled = await customIndexer.onFileChange(absPath, detectEvent(absPath));
@@ -2033,11 +2009,11 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
2033
2009
  } finally {
2034
2010
  flushing = false;
2035
2011
  if (pending.size > 0) {
2036
- timer = setTimeout(() => flush(), debounceMs);
2012
+ timer = setTimeout(() => processPending(), debounceMs);
2037
2013
  }
2038
2014
  }
2039
2015
  }
2040
- __name(flush, "flush");
2016
+ __name(processPending, "processPending");
2041
2017
  function detectEvent(filePath) {
2042
2018
  try {
2043
2019
  fs2.accessSync(filePath);
@@ -2055,7 +2031,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
2055
2031
  }
2056
2032
  if (isIgnoredFile(path3.basename(filename))) return false;
2057
2033
  if (isSupported(filename)) return true;
2058
- if (matchCustomIndexer(path3.resolve(repoPath, filename))) return true;
2034
+ if (matchCustomPlugin(path3.resolve(repoPath, filename))) return true;
2059
2035
  return false;
2060
2036
  }
2061
2037
  __name(shouldWatch, "shouldWatch");
@@ -2068,7 +2044,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
2068
2044
  if (!shouldWatch(filename)) return;
2069
2045
  pending.add(filename);
2070
2046
  if (timer) clearTimeout(timer);
2071
- timer = setTimeout(() => flush(), debounceMs);
2047
+ timer = setTimeout(() => processPending(), debounceMs);
2072
2048
  });
2073
2049
  watcher.on("error", (err) => {
2074
2050
  onError?.(err instanceof Error ? err : new Error(String(err)));
@@ -2101,7 +2077,7 @@ var BrainBank = class extends EventEmitter {
2101
2077
  _config;
2102
2078
  _db;
2103
2079
  _embedding;
2104
- _registry = new IndexerRegistry();
2080
+ _registry = new PluginRegistry();
2105
2081
  _searchAPI;
2106
2082
  _indexAPI;
2107
2083
  _initialized = false;
@@ -2117,28 +2093,28 @@ var BrainBank = class extends EventEmitter {
2117
2093
  super();
2118
2094
  this._config = resolveConfig(config);
2119
2095
  }
2120
- // ── Indexer registration ─────────────────────────
2096
+ // ── Plugin registration ──────────────────────────
2121
2097
  /**
2122
- * Register an indexer. Chainable.
2098
+ * Register a plugin. Chainable.
2123
2099
  *
2124
2100
  * brain.use(code({ repoPath: '.' })).use(docs());
2125
2101
  */
2126
- use(indexer) {
2102
+ use(plugin) {
2127
2103
  if (this._initialized)
2128
- throw new Error(`BrainBank: Cannot add indexer '${indexer.name}' after initialization. Call .use() before any operations.`);
2129
- this._registry.register(indexer);
2104
+ throw new Error(`BrainBank: Cannot add plugin '${plugin.name}' after initialization. Call .use() before any operations.`);
2105
+ this._registry.register(plugin);
2130
2106
  return this;
2131
2107
  }
2132
- /** Get the list of registered indexer names. */
2133
- get indexers() {
2108
+ /** Get the list of registered plugin names. */
2109
+ get plugins() {
2134
2110
  return this._registry.names;
2135
2111
  }
2136
- /** Check if an indexer is loaded. Also matches type prefix (e.g. 'code' matches 'code:frontend'). */
2112
+ /** Check if a plugin is loaded. Also matches type prefix (e.g. 'code' matches 'code:frontend'). */
2137
2113
  has(name) {
2138
2114
  return this._registry.has(name);
2139
2115
  }
2140
- /** Get an indexer instance. Throws if not loaded. */
2141
- indexer(n) {
2116
+ /** Get a plugin instance. Throws if not loaded. */
2117
+ plugin(n) {
2142
2118
  return this._registry.get(n);
2143
2119
  }
2144
2120
  // ── Initialization ───────────────────────────────
@@ -2176,13 +2152,13 @@ var BrainBank = class extends EventEmitter {
2176
2152
  }
2177
2153
  async _runInitialize(options = {}) {
2178
2154
  if (this._initialized) return;
2179
- const early = await earlyInit(this._config, (e, d) => this.emit(e, d), options);
2155
+ const initializer = new Initializer(this._config, (e, d) => this.emit(e, d));
2156
+ const early = await initializer.early(options);
2180
2157
  this._db = early.db;
2181
2158
  this._embedding = early.embedding;
2182
2159
  this._kvHnsw = early.kvHnsw;
2183
- const late = await lateInit(
2160
+ const late = await initializer.late(
2184
2161
  early,
2185
- this._config,
2186
2162
  this._registry,
2187
2163
  this._sharedHnsw,
2188
2164
  this._kvVecs,
@@ -2192,7 +2168,10 @@ var BrainBank = class extends EventEmitter {
2192
2168
  ...late,
2193
2169
  registry: this._registry,
2194
2170
  config: this._config,
2195
- searchDocs: /* @__PURE__ */ __name((q, o) => this.searchDocs(q, o), "searchDocs"),
2171
+ getDocsPlugin: /* @__PURE__ */ __name(() => {
2172
+ const docs = this._registry.get("docs");
2173
+ return docs && isCollectionPlugin(docs) ? docs : void 0;
2174
+ }, "getDocsPlugin"),
2196
2175
  collection: /* @__PURE__ */ __name((n) => this.collection(n), "collection")
2197
2176
  });
2198
2177
  this._indexAPI = new IndexAPI({
@@ -2201,7 +2180,7 @@ var BrainBank = class extends EventEmitter {
2201
2180
  emit: /* @__PURE__ */ __name((e, d) => this.emit(e, d), "emit")
2202
2181
  });
2203
2182
  this._initialized = true;
2204
- this.emit("initialized", { indexers: this.indexers });
2183
+ this.emit("initialized", { plugins: this.plugins });
2205
2184
  }
2206
2185
  // ── Collections (KV) ────────────────────────────
2207
2186
  /**
@@ -2336,13 +2315,13 @@ var BrainBank = class extends EventEmitter {
2336
2315
  /** Get git history for a specific file. */
2337
2316
  async fileHistory(filePath, limit = 20) {
2338
2317
  await this.initialize();
2339
- const gitPlugin = this.indexer("git");
2318
+ const gitPlugin = this.plugin("git");
2340
2319
  return gitPlugin.fileHistory(filePath, limit);
2341
2320
  }
2342
2321
  /** Get co-edit suggestions for a file. */
2343
2322
  coEdits(filePath, limit = 5) {
2344
2323
  this._requireInit("coEdits");
2345
- const gitPlugin = this.indexer("git");
2324
+ const gitPlugin = this.plugin("git");
2346
2325
  return gitPlugin.suggestCoEdits(filePath, limit);
2347
2326
  }
2348
2327
  // ── Stats ────────────────────────────────────────
@@ -2444,11 +2423,10 @@ export {
2444
2423
  resolveConfig,
2445
2424
  Collection,
2446
2425
  HNSWIndex,
2447
- LocalEmbedding,
2448
2426
  searchMMR,
2449
2427
  VectorSearch,
2450
2428
  KeywordSearch,
2451
2429
  ContextBuilder,
2452
2430
  BrainBank
2453
2431
  };
2454
- //# sourceMappingURL=chunk-MY36UPPQ.js.map
2432
+ //# sourceMappingURL=chunk-DI3H6JVZ.js.map