brainbank 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +174 -15
  2. package/assets/architecture.png +0 -0
  3. package/dist/{base-4SUgeRWT.d.ts → base-DZWtdgIf.d.ts} +23 -27
  4. package/dist/chunk-6XOXM7MI.js +136 -0
  5. package/dist/chunk-6XOXM7MI.js.map +1 -0
  6. package/dist/{chunk-FINIFKAY.js → chunk-BNV43SEF.js} +5 -4
  7. package/dist/chunk-BNV43SEF.js.map +1 -0
  8. package/dist/{chunk-MGIFEPYZ.js → chunk-DDECTPRM.js} +22 -17
  9. package/dist/chunk-DDECTPRM.js.map +1 -0
  10. package/dist/{chunk-5VUYPNH3.js → chunk-HNPABX7L.js} +6 -3
  11. package/dist/chunk-HNPABX7L.js.map +1 -0
  12. package/dist/{chunk-2BEWWQL2.js → chunk-MY36UPPQ.js} +227 -112
  13. package/dist/chunk-MY36UPPQ.js.map +1 -0
  14. package/dist/chunk-N2OJRXSB.js +117 -0
  15. package/dist/chunk-N2OJRXSB.js.map +1 -0
  16. package/dist/{chunk-FI7GWG4W.js → chunk-TTXVJFAE.js} +5 -2
  17. package/dist/chunk-TTXVJFAE.js.map +1 -0
  18. package/dist/{chunk-QNHBCOKB.js → chunk-U2Q2XGPZ.js} +7 -2
  19. package/dist/{chunk-QNHBCOKB.js.map → chunk-U2Q2XGPZ.js.map} +1 -1
  20. package/dist/{chunk-E6WQM4DN.js → chunk-YOLKSYWK.js} +1 -1
  21. package/dist/chunk-YOLKSYWK.js.map +1 -0
  22. package/dist/{chunk-Y3JKI6QN.js → chunk-YRGUIRN5.js} +234 -57
  23. package/dist/chunk-YRGUIRN5.js.map +1 -0
  24. package/dist/cli.js +21 -10
  25. package/dist/cli.js.map +1 -1
  26. package/dist/code.d.ts +1 -1
  27. package/dist/code.js +2 -1
  28. package/dist/docs.d.ts +1 -1
  29. package/dist/docs.js +2 -1
  30. package/dist/git.d.ts +1 -1
  31. package/dist/git.js +2 -1
  32. package/dist/index.d.ts +100 -4
  33. package/dist/index.js +16 -8
  34. package/dist/index.js.map +1 -1
  35. package/dist/memory.d.ts +1 -1
  36. package/dist/memory.js +2 -2
  37. package/dist/notes.d.ts +1 -1
  38. package/dist/notes.js +3 -2
  39. package/dist/perplexity-context-embedding-KSVSZXMD.js +9 -0
  40. package/dist/perplexity-context-embedding-KSVSZXMD.js.map +1 -0
  41. package/dist/perplexity-embedding-227WQY4R.js +10 -0
  42. package/dist/perplexity-embedding-227WQY4R.js.map +1 -0
  43. package/package.json +1 -1
  44. package/dist/chunk-2BEWWQL2.js.map +0 -1
  45. package/dist/chunk-5VUYPNH3.js.map +0 -1
  46. package/dist/chunk-E6WQM4DN.js.map +0 -1
  47. package/dist/chunk-FI7GWG4W.js.map +0 -1
  48. package/dist/chunk-FINIFKAY.js.map +0 -1
  49. package/dist/chunk-MGIFEPYZ.js.map +0 -1
  50. package/dist/chunk-Y3JKI6QN.js.map +0 -1
@@ -2,15 +2,19 @@ import {
2
2
  isIgnoredDir,
3
3
  isIgnoredFile,
4
4
  isSupported
5
- } from "./chunk-MGIFEPYZ.js";
5
+ } from "./chunk-DDECTPRM.js";
6
+ import {
7
+ rerank
8
+ } from "./chunk-YRGUIRN5.js";
6
9
  import {
7
10
  normalizeBM25,
8
11
  reciprocalRankFusion,
9
12
  sanitizeFTS
10
- } from "./chunk-E6WQM4DN.js";
13
+ } from "./chunk-YOLKSYWK.js";
11
14
  import {
12
- cosineSimilarity
13
- } from "./chunk-QNHBCOKB.js";
15
+ cosineSimilarity,
16
+ vecToBuffer
17
+ } from "./chunk-U2Q2XGPZ.js";
14
18
  import {
15
19
  __name
16
20
  } from "./chunk-7QVYU63E.js";
@@ -81,7 +85,7 @@ var Collection = class {
81
85
  const id = Number(result.lastInsertRowid);
82
86
  this._db.prepare(
83
87
  "INSERT INTO kv_vectors (data_id, embedding) VALUES (?, ?)"
84
- ).run(id, Buffer.from(vec.buffer));
88
+ ).run(id, vecToBuffer(vec));
85
89
  this._hnsw.add(vec, id);
86
90
  this._vecs.set(id, vec);
87
91
  return id;
@@ -110,7 +114,7 @@ var Collection = class {
110
114
  expiresAt
111
115
  );
112
116
  const id = Number(result.lastInsertRowid);
113
- insertVec.run(id, Buffer.from(vecs[i].buffer));
117
+ insertVec.run(id, vecToBuffer(vecs[i]));
114
118
  ids.push(id);
115
119
  }
116
120
  });
@@ -146,12 +150,15 @@ var Collection = class {
146
150
  if (results.length >= k) break;
147
151
  }
148
152
  if (this._reranker && results.length > 1) {
149
- const documents = results.map((r) => r.content);
150
- const scores = await this._reranker.rank(query, documents);
151
- const blended = results.map((r, i) => ({
152
- ...r,
153
- score: 0.6 * (r.score ?? 0) + 0.4 * (scores[i] ?? 0)
153
+ const asSearchResults = results.map((r) => ({
154
+ type: "collection",
155
+ score: r.score ?? 0,
156
+ content: r.content,
157
+ metadata: { id: r.id }
154
158
  }));
159
+ const reranked = await rerank(query, asSearchResults, this._reranker);
160
+ const rerankedById = new Map(reranked.map((r) => [r.metadata?.id, r.score]));
161
+ const blended = results.map((r) => ({ ...r, score: rerankedById.get(r.id) ?? r.score ?? 0 }));
155
162
  return this._filterByTags(
156
163
  blended.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
157
164
  tags
@@ -216,19 +223,14 @@ var Collection = class {
216
223
  }
217
224
  // ── Private ──────────────────────────────────────
218
225
  _removeById(id) {
219
- this._vecs.delete(id);
220
- this._hnsw.remove(id);
221
226
  this._db.prepare("DELETE FROM kv_data WHERE id = ?").run(id);
227
+ this._hnsw.remove(id);
228
+ this._vecs.delete(id);
222
229
  }
223
230
  async _searchVector(query, k, minScore) {
224
231
  if (this._hnsw.size === 0) return [];
225
232
  const queryVec = await this._embedding.embed(query);
226
- const now = Math.floor(Date.now() / 1e3);
227
- const collectionCount = this._db.prepare(
228
- "SELECT COUNT(*) as c FROM kv_data WHERE collection = ? AND (expires_at IS NULL OR expires_at > ?)"
229
- ).get(this._name, now)?.c ?? 0;
230
- const ratio = collectionCount > 0 ? Math.max(3, Math.min(50, Math.ceil(this._hnsw.size / collectionCount))) : 3;
231
- const searchK = Math.min(k * ratio, this._hnsw.size);
233
+ const searchK = Math.min(k * 10, this._hnsw.size);
232
234
  const hits = this._hnsw.search(queryVec, searchK);
233
235
  const ids = hits.map((h) => h.id);
234
236
  if (ids.length === 0) return [];
@@ -308,6 +310,7 @@ function parseDuration(s) {
308
310
  __name(parseDuration, "parseDuration");
309
311
 
310
312
  // src/providers/vector/hnsw-index.ts
313
+ import { existsSync } from "fs";
311
314
  var HNSWIndex = class {
312
315
  constructor(_dims, _maxElements = 2e6, _M = 16, _efConstruction = 200, _efSearch = 50) {
313
316
  this._dims = _dims;
@@ -398,6 +401,38 @@ var HNSWIndex = class {
398
401
  get size() {
399
402
  return this._ids.size;
400
403
  }
404
+ /**
405
+ * Save the HNSW graph to disk.
406
+ * The file can be loaded later with tryLoad() to skip vector-by-vector insertion.
407
+ */
408
+ save(path4) {
409
+ if (!this._index || this._ids.size === 0) return;
410
+ this._index.writeIndexSync(path4);
411
+ }
412
+ /**
413
+ * Try to load a previously saved HNSW index from disk.
414
+ * Returns true if loaded successfully, false if stale or missing.
415
+ * @param path File path to the saved index
416
+ * @param expectedCount Expected number of vectors (from SQLite) — used to detect staleness
417
+ */
418
+ tryLoad(path4, expectedCount) {
419
+ if (!this._index || !existsSync(path4)) return false;
420
+ try {
421
+ this._index.readIndexSync(path4);
422
+ const loadedCount = this._index.getCurrentCount();
423
+ if (loadedCount !== expectedCount) {
424
+ this.reinit();
425
+ return false;
426
+ }
427
+ const ids = this._index.getIdsList();
428
+ this._ids = new Set(ids);
429
+ this._index.setEf(this._efSearch);
430
+ return true;
431
+ } catch {
432
+ this.reinit();
433
+ return false;
434
+ }
435
+ }
401
436
  };
402
437
 
403
438
  // src/providers/embeddings/local-embedding.ts
@@ -460,7 +495,7 @@ var LocalEmbedding = class {
460
495
  const output = await pipe(batch, { pooling: "mean", normalize: true });
461
496
  for (let j = 0; j < batch.length; j++) {
462
497
  const start = j * this.dims;
463
- results.push(new Float32Array(output.data.buffer, start * 4, this.dims));
498
+ results.push(output.data.slice(start, start + this.dims));
464
499
  }
465
500
  }
466
501
  return results;
@@ -502,22 +537,6 @@ function searchMMR(index, query, vectorCache, k, lambda = 0.7) {
502
537
  }
503
538
  __name(searchMMR, "searchMMR");
504
539
 
505
- // src/search/vector/rerank.ts
506
- async function rerank(query, results, reranker) {
507
- const documents = results.map((r) => r.content);
508
- const scores = await reranker.rank(query, documents);
509
- const blended = results.map((r, i) => {
510
- const pos = i + 1;
511
- const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.6 : 0.4;
512
- return {
513
- ...r,
514
- score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0)
515
- };
516
- });
517
- return blended.sort((a, b) => b.score - a.score);
518
- }
519
- __name(rerank, "rerank");
520
-
521
540
  // src/search/vector/vector-search.ts
522
541
  var VectorSearch = class {
523
542
  static {
@@ -645,6 +664,10 @@ var VectorSearch = class {
645
664
  };
646
665
 
647
666
  // src/search/keyword/keyword-search.ts
667
+ function isFTSError(e) {
668
+ return e instanceof Error && /fts5|syntax error|parse error/i.test(e.message);
669
+ }
670
+ __name(isFTSError, "isFTSError");
648
671
  var KeywordSearch = class {
649
672
  constructor(_db) {
650
673
  this._db = _db;
@@ -683,7 +706,8 @@ var KeywordSearch = class {
683
706
  seenIds.add(r.id);
684
707
  results.push(this._toCodeResult(r, normalizeBM25(r.score), "bm25"));
685
708
  }
686
- } catch {
709
+ } catch (e) {
710
+ if (!isFTSError(e)) throw e;
687
711
  }
688
712
  this._searchCodeByPath(rawQuery, seenIds, results);
689
713
  }
@@ -704,7 +728,8 @@ var KeywordSearch = class {
704
728
  results.push(this._toCodeResult(r, 0.6, "bm25-path"));
705
729
  }
706
730
  }
707
- } catch {
731
+ } catch (e) {
732
+ if (!isFTSError(e)) throw e;
708
733
  }
709
734
  }
710
735
  /** FTS5 search across git commits. */
@@ -738,7 +763,8 @@ var KeywordSearch = class {
738
763
  }
739
764
  });
740
765
  }
741
- } catch {
766
+ } catch (e) {
767
+ if (!isFTSError(e)) throw e;
742
768
  }
743
769
  }
744
770
  /** FTS5 search across memory patterns. */
@@ -769,7 +795,8 @@ var KeywordSearch = class {
769
795
  }
770
796
  });
771
797
  }
772
- } catch {
798
+ } catch (e) {
799
+ if (!isFTSError(e)) throw e;
773
800
  }
774
801
  }
775
802
  /** Map a code_chunks row to a CodeResult. */
@@ -1002,6 +1029,9 @@ var IndexerRegistry = class {
1002
1029
  }
1003
1030
  };
1004
1031
 
1032
+ // src/core/initializer.ts
1033
+ import { dirname as dirname2, join as join2 } from "path";
1034
+
1005
1035
  // src/db/database.ts
1006
1036
  import BetterSqlite3 from "better-sqlite3";
1007
1037
  import * as fs from "fs";
@@ -1484,24 +1514,34 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1484
1514
  `SELECT COUNT(*) as c FROM ${table.textTable}`
1485
1515
  ).get().c;
1486
1516
  if (totalCount === 0) return 0;
1487
- const insertVec = db.prepare(
1488
- `INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
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 (?, ?)`
1489
1522
  );
1490
- db.prepare(`DELETE FROM ${table.vectorTable}`).run();
1491
1523
  let processed = 0;
1492
- for (let offset = 0; offset < totalCount; offset += batchSize) {
1493
- const batch = db.prepare(
1494
- `SELECT * FROM ${table.textTable} LIMIT ? OFFSET ?`
1495
- ).all(batchSize, offset);
1496
- const texts = batch.map((r) => table.textBuilder(r));
1497
- const vectors = await embedding.embedBatch(texts);
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
+ }
1498
1539
  db.transaction(() => {
1499
- for (let j = 0; j < batch.length; j++) {
1500
- insertVec.run(batch[j][table.idColumn], Buffer.from(vectors[j].buffer));
1501
- }
1540
+ db.exec(`DELETE FROM ${table.vectorTable}`);
1541
+ db.exec(`INSERT INTO ${table.vectorTable} SELECT * FROM ${tempTable}`);
1502
1542
  });
1503
- processed += batch.length;
1504
- onProgress?.(table.name, processed, totalCount);
1543
+ } finally {
1544
+ db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
1505
1545
  }
1506
1546
  return processed;
1507
1547
  }
@@ -1584,12 +1624,19 @@ __name(earlyInit, "earlyInit");
1584
1624
  async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollection) {
1585
1625
  const { db, embedding, kvHnsw, skipVectorLoad } = early;
1586
1626
  if (!skipVectorLoad) {
1587
- loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
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);
1633
+ }
1588
1634
  }
1589
1635
  const ctx = buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection);
1590
1636
  for (const mod of registry.all) {
1591
1637
  await mod.initialize(ctx);
1592
1638
  }
1639
+ saveAllHnsw(config.dbPath, kvHnsw, sharedHnsw);
1593
1640
  return buildSearchLayer(db, embedding, config, registry, sharedHnsw);
1594
1641
  }
1595
1642
  __name(lateInit, "lateInit");
@@ -1607,7 +1654,14 @@ function buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad,
1607
1654
  ).init(), "createHnsw"),
1608
1655
  loadVectors: /* @__PURE__ */ __name((table, idCol, hnsw, cache) => {
1609
1656
  if (skipVectorLoad) return;
1610
- loadVectors(db, table, idCol, hnsw, cache);
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);
1662
+ } else {
1663
+ loadVectors(db, table, idCol, hnsw, cache);
1664
+ }
1611
1665
  }, "loadVectors"),
1612
1666
  getOrCreateSharedHnsw: /* @__PURE__ */ __name(async (type, maxElements) => {
1613
1667
  const existing = sharedHnsw.get(type);
@@ -1649,9 +1703,28 @@ function buildSearchLayer(db, embedding, config, registry, sharedHnsw) {
1649
1703
  return { search, bm25, contextBuilder };
1650
1704
  }
1651
1705
  __name(buildSearchLayer, "buildSearchLayer");
1706
+ function hnswPath(dbPath, name) {
1707
+ return join2(dirname2(dbPath), `hnsw-${name}.index`);
1708
+ }
1709
+ __name(hnswPath, "hnswPath");
1710
+ function countRows(db, table) {
1711
+ const row = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get();
1712
+ return row?.c ?? 0;
1713
+ }
1714
+ __name(countRows, "countRows");
1715
+ function saveAllHnsw(dbPath, kvHnsw, sharedHnsw) {
1716
+ try {
1717
+ kvHnsw.save(hnswPath(dbPath, "kv"));
1718
+ for (const [name, { hnsw }] of sharedHnsw) {
1719
+ hnsw.save(hnswPath(dbPath, name));
1720
+ }
1721
+ } catch {
1722
+ }
1723
+ }
1724
+ __name(saveAllHnsw, "saveAllHnsw");
1652
1725
  function loadVectors(db, table, idCol, hnsw, cache) {
1653
- const rows = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).all();
1654
- for (const row of rows) {
1726
+ const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
1727
+ for (const row of iter) {
1655
1728
  const vec = new Float32Array(
1656
1729
  row.embedding.buffer.slice(
1657
1730
  row.embedding.byteOffset,
@@ -1663,6 +1736,19 @@ function loadVectors(db, table, idCol, hnsw, cache) {
1663
1736
  }
1664
1737
  }
1665
1738
  __name(loadVectors, "loadVectors");
1739
+ function loadVecCache(db, table, idCol, cache) {
1740
+ const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
1741
+ for (const row of iter) {
1742
+ const vec = new Float32Array(
1743
+ row.embedding.buffer.slice(
1744
+ row.embedding.byteOffset,
1745
+ row.embedding.byteOffset + row.embedding.byteLength
1746
+ )
1747
+ );
1748
+ cache.set(row[idCol], vec);
1749
+ }
1750
+ }
1751
+ __name(loadVecCache, "loadVecCache");
1666
1752
 
1667
1753
  // src/core/search-api.ts
1668
1754
  var SearchAPI = class {
@@ -1735,11 +1821,7 @@ var SearchAPI = class {
1735
1821
  /** Apply reranking if a reranker is configured. */
1736
1822
  async _applyReranking(query, fused) {
1737
1823
  if (!this._d.config.reranker || fused.length <= 1) return fused;
1738
- const scores = await this._d.config.reranker.rank(query, fused.map((r) => r.content));
1739
- return fused.map((r, i) => {
1740
- const w = i < 3 ? 0.75 : i < 10 ? 0.6 : 0.4;
1741
- return { ...r, score: w * r.score + (1 - w) * (scores[i] ?? 0) };
1742
- }).sort((a, b) => b.score - a.score);
1824
+ return rerank(query, fused, this._d.config.reranker);
1743
1825
  }
1744
1826
  // ── Keyword ─────────────────────────────────────
1745
1827
  async searchBM25(query, options) {
@@ -1774,6 +1856,20 @@ ${body}`);
1774
1856
  }
1775
1857
  };
1776
1858
 
1859
+ // src/indexers/base.ts
1860
+ function isIndexable(i) {
1861
+ return typeof i.index === "function";
1862
+ }
1863
+ __name(isIndexable, "isIndexable");
1864
+ function isWatchable(i) {
1865
+ return typeof i.onFileChange === "function" && typeof i.watchPatterns === "function";
1866
+ }
1867
+ __name(isWatchable, "isWatchable");
1868
+ function isCollectionPlugin(i) {
1869
+ return typeof i.addCollection === "function" && typeof i.listCollections === "function";
1870
+ }
1871
+ __name(isCollectionPlugin, "isCollectionPlugin");
1872
+
1777
1873
  // src/core/index-api.ts
1778
1874
  var IndexAPI = class {
1779
1875
  constructor(_d) {
@@ -1787,6 +1883,7 @@ var IndexAPI = class {
1787
1883
  const result = {};
1788
1884
  if (want.has("code")) {
1789
1885
  for (const mod of this._d.registry.allByType("code")) {
1886
+ if (!isIndexable(mod)) continue;
1790
1887
  const label = mod.name === "code" ? "code" : mod.name;
1791
1888
  options.onProgress?.(label, "Starting...");
1792
1889
  const r = await mod.index({
@@ -1804,6 +1901,7 @@ var IndexAPI = class {
1804
1901
  }
1805
1902
  if (want.has("git")) {
1806
1903
  for (const mod of this._d.registry.allByType("git")) {
1904
+ if (!isIndexable(mod)) continue;
1807
1905
  const label = mod.name === "git" ? "git" : mod.name;
1808
1906
  options.onProgress?.(label, "Starting...");
1809
1907
  const r = await mod.index({
@@ -1819,16 +1917,19 @@ var IndexAPI = class {
1819
1917
  }
1820
1918
  }
1821
1919
  if (want.has("docs") && this._d.registry.has("docs")) {
1822
- options.onProgress?.("docs", "Starting...");
1823
- result.docs = await this._d.registry.get("docs").indexCollections({
1824
- onProgress: /* @__PURE__ */ __name((coll, file, cur, total) => options.onProgress?.("docs", `[${coll}] ${cur}/${total}: ${file}`), "onProgress")
1825
- });
1920
+ const docsPlugin = this._d.registry.get("docs");
1921
+ if (isCollectionPlugin(docsPlugin)) {
1922
+ options.onProgress?.("docs", "Starting...");
1923
+ result.docs = await docsPlugin.indexCollections({
1924
+ onProgress: /* @__PURE__ */ __name((coll, file, cur, total) => options.onProgress?.("docs", `[${coll}] ${cur}/${total}: ${file}`), "onProgress")
1925
+ });
1926
+ }
1826
1927
  }
1827
1928
  this._d.emit("indexed", result);
1828
1929
  return result;
1829
1930
  }
1830
1931
  async indexCode(options = {}) {
1831
- const mods = this._d.registry.allByType("code");
1932
+ const mods = this._d.registry.allByType("code").filter(isIndexable);
1832
1933
  if (!mods.length) throw new Error("BrainBank: Indexer 'code' is not loaded. Add .use(code()) to your BrainBank instance.");
1833
1934
  const acc = { indexed: 0, skipped: 0, chunks: 0 };
1834
1935
  for (const mod of mods) {
@@ -1840,7 +1941,7 @@ var IndexAPI = class {
1840
1941
  return acc;
1841
1942
  }
1842
1943
  async indexGit(options = {}) {
1843
- const mods = this._d.registry.allByType("git");
1944
+ const mods = this._d.registry.allByType("git").filter(isIndexable);
1844
1945
  if (!mods.length) throw new Error("BrainBank: Indexer 'git' is not loaded. Add .use(git()) to your BrainBank instance.");
1845
1946
  const acc = { indexed: 0, skipped: 0 };
1846
1947
  for (const mod of mods) {
@@ -1868,7 +1969,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1868
1969
  let timer = null;
1869
1970
  const customPatterns = [];
1870
1971
  for (const indexer of indexers.values()) {
1871
- if (indexer.watchPatterns) {
1972
+ if (isWatchable(indexer)) {
1872
1973
  customPatterns.push({ indexer, patterns: indexer.watchPatterns() });
1873
1974
  }
1874
1975
  }
@@ -1895,35 +1996,44 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1895
1996
  return filePath === pattern;
1896
1997
  }
1897
1998
  __name(matchGlob, "matchGlob");
1999
+ let flushing = false;
1898
2000
  async function flush() {
1899
- if (pending.size === 0) return;
1900
- const files = [...pending];
1901
- pending.clear();
1902
- let needsReindex = false;
1903
- for (const filePath of files) {
1904
- const absPath = path3.resolve(repoPath, filePath);
1905
- const customIndexer = matchCustomIndexer(absPath);
1906
- if (customIndexer?.onFileChange) {
1907
- try {
1908
- const handled = await customIndexer.onFileChange(absPath, detectEvent(absPath));
1909
- if (handled) {
1910
- onIndex?.(filePath, customIndexer.name);
1911
- continue;
2001
+ if (flushing || pending.size === 0) return;
2002
+ flushing = true;
2003
+ try {
2004
+ const files = [...pending];
2005
+ pending.clear();
2006
+ let needsReindex = false;
2007
+ for (const filePath of files) {
2008
+ const absPath = path3.resolve(repoPath, filePath);
2009
+ const customIndexer = matchCustomIndexer(absPath);
2010
+ if (customIndexer && isWatchable(customIndexer)) {
2011
+ try {
2012
+ const handled = await customIndexer.onFileChange(absPath, detectEvent(absPath));
2013
+ if (handled) {
2014
+ onIndex?.(filePath, customIndexer.name);
2015
+ continue;
2016
+ }
2017
+ } catch (err) {
2018
+ onError?.(err instanceof Error ? err : new Error(String(err)));
1912
2019
  }
2020
+ }
2021
+ if (isSupported(filePath)) {
2022
+ needsReindex = true;
2023
+ onIndex?.(filePath, "code");
2024
+ }
2025
+ }
2026
+ if (needsReindex) {
2027
+ try {
2028
+ await reindexFn();
1913
2029
  } catch (err) {
1914
2030
  onError?.(err instanceof Error ? err : new Error(String(err)));
1915
2031
  }
1916
2032
  }
1917
- if (isSupported(filePath)) {
1918
- needsReindex = true;
1919
- onIndex?.(filePath, "code");
1920
- }
1921
- }
1922
- if (needsReindex) {
1923
- try {
1924
- await reindexFn();
1925
- } catch (err) {
1926
- onError?.(err instanceof Error ? err : new Error(String(err)));
2033
+ } finally {
2034
+ flushing = false;
2035
+ if (pending.size > 0) {
2036
+ timer = setTimeout(() => flush(), debounceMs);
1927
2037
  }
1928
2038
  }
1929
2039
  }
@@ -2115,6 +2225,12 @@ var BrainBank = class extends EventEmitter {
2115
2225
  this._requireInit("listCollectionNames");
2116
2226
  return this._db.prepare("SELECT DISTINCT collection FROM kv_data ORDER BY collection").all().map((r) => r.collection);
2117
2227
  }
2228
+ /** Delete a collection's data and evict from cache. */
2229
+ deleteCollection(name) {
2230
+ this._requireInit("deleteCollection");
2231
+ this._db.prepare("DELETE FROM kv_data WHERE collection = ?").run(name);
2232
+ this._collections.delete(name);
2233
+ }
2118
2234
  // ── Indexing (delegated to IndexAPI) ─────────────
2119
2235
  async index(options = {}) {
2120
2236
  await this.initialize();
@@ -2134,26 +2250,21 @@ var BrainBank = class extends EventEmitter {
2134
2250
  /** Register a document collection. */
2135
2251
  async addCollection(collection) {
2136
2252
  await this.initialize();
2137
- this._requireDocs("addCollection");
2138
- this.indexer("docs").addCollection(collection);
2253
+ this._docsPlugin("addCollection").addCollection(collection);
2139
2254
  }
2140
2255
  /** Remove a collection and all its indexed data. */
2141
2256
  async removeCollection(name) {
2142
2257
  await this.initialize();
2143
- this._requireDocs("removeCollection");
2144
- this.indexer("docs").removeCollection(name);
2258
+ this._docsPlugin("removeCollection").removeCollection(name);
2145
2259
  }
2146
2260
  /** List all registered collections. */
2147
2261
  listCollections() {
2148
- this._requireInit("listCollections");
2149
- this._requireDocs("listCollections");
2150
- return this.indexer("docs").listCollections();
2262
+ return this._docsPlugin("listCollections").listCollections();
2151
2263
  }
2152
2264
  /** Index all (or specific) document collections. */
2153
2265
  async indexDocs(options = {}) {
2154
2266
  await this.initialize();
2155
- this._requireDocs("indexDocs");
2156
- const results = await this.indexer("docs").indexCollections(options);
2267
+ const results = await this._docsPlugin("indexDocs").indexCollections(options);
2157
2268
  this.emit("docsIndexed", results);
2158
2269
  return results;
2159
2270
  }
@@ -2161,23 +2272,23 @@ var BrainBank = class extends EventEmitter {
2161
2272
  async searchDocs(query, options) {
2162
2273
  await this.initialize();
2163
2274
  if (!this.has("docs")) return [];
2164
- return this.indexer("docs").search(query, options);
2275
+ return this._docsPlugin("searchDocs").search(query, options);
2165
2276
  }
2166
2277
  // ── Context metadata ─────────────────────────────
2167
2278
  /** Add context description for a collection path. */
2168
2279
  addContext(collection, path4, context) {
2169
- this._requireDocs("addContext");
2170
- this.indexer("docs").addContext(collection, path4, context);
2280
+ const docs = this._docsPlugin("addContext");
2281
+ if (docs.addContext) docs.addContext(collection, path4, context);
2171
2282
  }
2172
2283
  /** Remove context for a collection path. */
2173
2284
  removeContext(collection, path4) {
2174
- this._requireDocs("removeContext");
2175
- this.indexer("docs").removeContext(collection, path4);
2285
+ const docs = this._docsPlugin("removeContext");
2286
+ if (docs.removeContext) docs.removeContext(collection, path4);
2176
2287
  }
2177
2288
  /** List all context entries. */
2178
2289
  listContexts() {
2179
- this._requireDocs("listContexts");
2180
- return this.indexer("docs").listContexts();
2290
+ const docs = this._docsPlugin("listContexts");
2291
+ return docs.listContexts?.() ?? [];
2181
2292
  }
2182
2293
  // ── Search (delegated to SearchAPI) ─────────────
2183
2294
  /**
@@ -2318,9 +2429,13 @@ var BrainBank = class extends EventEmitter {
2318
2429
  if (!this._initialized)
2319
2430
  throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
2320
2431
  }
2321
- _requireDocs(method) {
2322
- if (!this.has("docs"))
2432
+ /** Get the docs indexer as CollectionPlugin with init + type check. */
2433
+ _docsPlugin(method) {
2434
+ this._requireInit(method);
2435
+ const docs = this._registry.get("docs");
2436
+ if (!docs || !isCollectionPlugin(docs))
2323
2437
  throw new Error(`BrainBank: Docs indexer not loaded. Add .use(docs()) before calling ${method}().`);
2438
+ return docs;
2324
2439
  }
2325
2440
  };
2326
2441
 
@@ -2336,4 +2451,4 @@ export {
2336
2451
  ContextBuilder,
2337
2452
  BrainBank
2338
2453
  };
2339
- //# sourceMappingURL=chunk-2BEWWQL2.js.map
2454
+ //# sourceMappingURL=chunk-MY36UPPQ.js.map