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
@@ -521,12 +521,12 @@ declare class Collection {
521
521
  }
522
522
 
523
523
  /**
524
- * BrainBank — Indexer System
524
+ * BrainBank — Plugin System
525
525
  *
526
- * Indexers are pluggable strategies that scan external data sources
527
- * and push content into BrainBank. Built-in indexers handle code,
526
+ * Plugins are pluggable strategies that scan external data sources
527
+ * and push content into BrainBank. Built-in plugins handle code,
528
528
  * git, and docs. Third-party frameworks (LangChain, etc.)
529
- * can implement custom indexers.
529
+ * can implement custom plugins.
530
530
  *
531
531
  * import { BrainBank } from 'brainbank';
532
532
  * import { code } from 'brainbank/indexers/code';
@@ -535,19 +535,19 @@ declare class Collection {
535
535
  * .use(code({ repoPath: '.' }));
536
536
  */
537
537
 
538
- interface IndexerContext {
539
- /** SQLite database (shared across all indexers). */
538
+ interface PluginContext {
539
+ /** SQLite database (shared across all plugins). */
540
540
  db: Database;
541
541
  /** Embedding provider (shared). */
542
542
  embedding: EmbeddingProvider;
543
543
  /** Resolved BrainBank config. */
544
544
  config: ResolvedConfig;
545
- /** Create and initialize an HNSW index. */
546
- createHnsw(maxElements?: number): Promise<HNSWIndex>;
545
+ /** Create and initialize an HNSW index. Optionally override dims for per-plugin embeddings. */
546
+ createHnsw(maxElements?: number, dims?: number): Promise<HNSWIndex>;
547
547
  /** Load existing vectors from a SQLite vectors table into an HNSW index + cache. */
548
548
  loadVectors(table: string, idCol: string, hnsw: HNSWIndex, cache: Map<number, Float32Array>): void;
549
- /** Get or create a shared HNSW index by type (e.g. 'code', 'git'). For multi-repo support. */
550
- getOrCreateSharedHnsw(type: string, maxElements?: number): Promise<{
549
+ /** Get or create a shared HNSW index by type (e.g. 'code', 'git'). Optionally override dims for per-plugin embeddings. */
550
+ getOrCreateSharedHnsw(type: string, maxElements?: number, dims?: number): Promise<{
551
551
  hnsw: HNSWIndex;
552
552
  vecCache: Map<number, Float32Array>;
553
553
  isNew: boolean;
@@ -555,31 +555,31 @@ interface IndexerContext {
555
555
  /** Get or create a dynamic collection. */
556
556
  collection(name: string): Collection;
557
557
  }
558
- interface Indexer {
559
- /** Unique indexer name (e.g. 'code', 'git', 'docs'). */
558
+ interface Plugin {
559
+ /** Unique plugin name (e.g. 'code', 'git', 'docs'). */
560
560
  readonly name: string;
561
- /** Initialize the indexer (create HNSW, load vectors, etc.). */
562
- initialize(ctx: IndexerContext): Promise<void>;
563
- /** Return stats for this indexer. */
561
+ /** Initialize the plugin (create HNSW, load vectors, etc.). */
562
+ initialize(ctx: PluginContext): Promise<void>;
563
+ /** Return stats for this plugin. */
564
564
  stats?(): Record<string, any>;
565
565
  /** Clean up resources. */
566
566
  close?(): void;
567
567
  }
568
- /** Indexers that can scan and index content (code, git). */
569
- interface IndexablePlugin extends Indexer {
568
+ /** Plugins that can scan and index content (code, git). */
569
+ interface IndexablePlugin extends Plugin {
570
570
  index(options?: any): Promise<any>;
571
571
  }
572
- /** Indexers that can search indexed content (docs). */
573
- interface SearchablePlugin extends Indexer {
572
+ /** Plugins that can search indexed content (docs). */
573
+ interface SearchablePlugin extends Plugin {
574
574
  search(query: string, options?: any): Promise<SearchResult[]>;
575
575
  }
576
- /** Indexers that support file watch mode. */
577
- interface WatchablePlugin extends Indexer {
576
+ /** Plugins that support file watch mode. */
577
+ interface WatchablePlugin extends Plugin {
578
578
  onFileChange(filePath: string, event: 'create' | 'update' | 'delete'): Promise<boolean>;
579
579
  watchPatterns(): string[];
580
580
  }
581
- /** Indexers that manage document collections. */
582
- interface CollectionPlugin extends Indexer {
581
+ /** Plugins that manage document collections. */
582
+ interface CollectionPlugin extends Plugin {
583
583
  addCollection(collection: DocumentCollection): void;
584
584
  removeCollection(name: string): void;
585
585
  listCollections(): DocumentCollection[];
@@ -590,4 +590,4 @@ interface CollectionPlugin extends Indexer {
590
590
  listContexts?(): any[];
591
591
  }
592
592
 
593
- export { type SearchResultType as A, type BrainBankConfig as B, Collection as C, type DocumentCollection as D, type EmbeddingProvider as E, type SearchablePlugin as F, type GitCommitRecord as G, HNSWIndex as H, type Indexer as I, isCodeResult as J, isCollectionResult as K, type LearningPattern as L, isCommitResult as M, isDocumentResult as N, isPatternResult as O, type ProgressCallback as P, matchResult as Q, type ResolvedConfig as R, type StageProgressCallback as S, type VectorIndex as V, type WatchablePlugin as W, type IndexResult as a, type SearchResult as b, type ContextOptions as c, type CoEditSuggestion as d, type IndexStats as e, type SearchHit as f, type CodeChunk as g, Database as h, type Reranker as i, type CodeResult as j, type CodeResultMetadata as k, type CollectionAddOptions as l, type CollectionItem as m, type CollectionPlugin as n, type CollectionResult as o, type CollectionSearchOptions as p, type CommitResult as q, type CommitResultMetadata as r, type DistilledStrategy as s, type DocChunk as t, type DocumentResult as u, type DocumentResultMetadata as v, type IndexablePlugin as w, type IndexerContext as x, type PatternResult as y, type PatternResultMetadata as z };
593
+ export { type SearchResultType as A, type BrainBankConfig as B, Collection as C, type DocumentCollection as D, type EmbeddingProvider as E, type SearchablePlugin as F, type GitCommitRecord as G, HNSWIndex as H, type IndexResult as I, isCodeResult as J, isCollectionResult as K, type LearningPattern as L, isCommitResult as M, isDocumentResult as N, isPatternResult as O, type Plugin as P, matchResult as Q, type ResolvedConfig as R, type StageProgressCallback as S, type VectorIndex as V, type WatchablePlugin as W, type ProgressCallback as a, type SearchResult as b, type ContextOptions as c, type CoEditSuggestion as d, type IndexStats as e, type Reranker as f, type SearchHit as g, type CodeChunk as h, Database as i, type CodeResult as j, type CodeResultMetadata as k, type CollectionAddOptions as l, type CollectionItem as m, type CollectionPlugin as n, type CollectionResult as o, type CollectionSearchOptions as p, type CommitResult as q, type CommitResultMetadata as r, type DistilledStrategy as s, type DocChunk as t, type DocumentResult as u, type DocumentResultMetadata as v, type IndexablePlugin as w, type PatternResult as x, type PatternResultMetadata as y, type PluginContext as z };
@@ -0,0 +1,78 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/providers/embeddings/local-embedding.ts
6
+ var LocalEmbedding = class {
7
+ static {
8
+ __name(this, "LocalEmbedding");
9
+ }
10
+ dims = 384;
11
+ _pipeline = null;
12
+ _modelName;
13
+ _cacheDir;
14
+ constructor(options = {}) {
15
+ this._modelName = options.model ?? "Xenova/all-MiniLM-L6-v2";
16
+ this._cacheDir = options.cacheDir ?? ".model-cache";
17
+ }
18
+ _pipelinePromise = null;
19
+ /**
20
+ * Lazy-load the transformer pipeline.
21
+ * Singleton — created once and reused.
22
+ * Promise-deduped to prevent concurrent downloads.
23
+ */
24
+ async _getPipeline() {
25
+ if (this._pipeline) return this._pipeline;
26
+ if (this._pipelinePromise) return this._pipelinePromise;
27
+ this._pipelinePromise = (async () => {
28
+ const { pipeline, env } = await import("@xenova/transformers");
29
+ env.cacheDir = this._cacheDir;
30
+ env.allowLocalModels = true;
31
+ this._pipeline = await pipeline("feature-extraction", this._modelName, {
32
+ quantized: true
33
+ });
34
+ return this._pipeline;
35
+ })();
36
+ try {
37
+ return await this._pipelinePromise;
38
+ } finally {
39
+ this._pipelinePromise = null;
40
+ }
41
+ }
42
+ /**
43
+ * Embed a single text string.
44
+ * Returns a normalized Float32Array of length 384.
45
+ */
46
+ async embed(text) {
47
+ const pipe = await this._getPipeline();
48
+ const output = await pipe(text, { pooling: "mean", normalize: true });
49
+ return output.data;
50
+ }
51
+ /**
52
+ * Embed multiple texts using real batch processing.
53
+ * Chunks into groups of BATCH_SIZE to balance throughput vs memory.
54
+ */
55
+ async embedBatch(texts) {
56
+ if (texts.length === 0) return [];
57
+ const BATCH_SIZE = 32;
58
+ const pipe = await this._getPipeline();
59
+ const results = [];
60
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
61
+ const batch = texts.slice(i, i + BATCH_SIZE);
62
+ const output = await pipe(batch, { pooling: "mean", normalize: true });
63
+ for (let j = 0; j < batch.length; j++) {
64
+ const start = j * this.dims;
65
+ results.push(output.data.slice(start, start + this.dims));
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+ async close() {
71
+ this._pipeline = null;
72
+ }
73
+ };
74
+
75
+ export {
76
+ LocalEmbedding
77
+ };
78
+ //# sourceMappingURL=chunk-424UFCY7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/embeddings/local-embedding.ts"],"sourcesContent":["/**\n * BrainBank — Local Embedding Provider\n * \n * Uses @xenova/transformers with all-MiniLM-L6-v2 (384 dims, WASM).\n * Downloads ~23MB on first use, cached locally.\n * No external API calls — runs entirely in-process.\n */\n\nimport type { EmbeddingProvider } from '@/types.ts';\n\nexport class LocalEmbedding implements EmbeddingProvider {\n readonly dims: number = 384;\n\n private _pipeline: any = null;\n private _modelName: string;\n private _cacheDir: string;\n\n constructor(options: { model?: string; cacheDir?: string } = {}) {\n this._modelName = options.model ?? 'Xenova/all-MiniLM-L6-v2';\n this._cacheDir = options.cacheDir ?? '.model-cache';\n }\n\n private _pipelinePromise: Promise<any> | null = null;\n\n /**\n * Lazy-load the transformer pipeline.\n * Singleton — created once and reused.\n * Promise-deduped to prevent concurrent downloads.\n */\n private async _getPipeline(): Promise<any> {\n if (this._pipeline) return this._pipeline;\n if (this._pipelinePromise) return this._pipelinePromise;\n\n this._pipelinePromise = (async () => {\n const { pipeline, env } = await import('@xenova/transformers' as any);\n env.cacheDir = this._cacheDir;\n env.allowLocalModels = true;\n\n this._pipeline = await pipeline('feature-extraction', this._modelName, {\n quantized: true,\n });\n\n return this._pipeline;\n })();\n\n try {\n return await this._pipelinePromise;\n } finally {\n this._pipelinePromise = null;\n }\n }\n\n /**\n * Embed a single text string.\n * Returns a normalized Float32Array of length 384.\n */\n async embed(text: string): Promise<Float32Array> {\n const pipe = await this._getPipeline();\n const output = await pipe(text, { pooling: 'mean', normalize: true });\n return output.data as Float32Array;\n }\n\n /**\n * Embed multiple texts using real batch processing.\n * Chunks into groups of BATCH_SIZE to balance throughput vs memory.\n */\n async embedBatch(texts: string[]): Promise<Float32Array[]> {\n if (texts.length === 0) return [];\n\n const BATCH_SIZE = 32;\n const pipe = await this._getPipeline();\n const results: Float32Array[] = [];\n\n for (let i = 0; i < texts.length; i += BATCH_SIZE) {\n const batch = texts.slice(i, i + BATCH_SIZE);\n const output = await pipe(batch, { pooling: 'mean', normalize: true });\n\n // output.data is a flat Float32Array — must copy, not view,\n // because the pipeline may reuse the underlying buffer\n for (let j = 0; j < batch.length; j++) {\n const start = j * this.dims;\n results.push(output.data.slice(start, start + this.dims) as Float32Array);\n }\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n this._pipeline = null;\n }\n}\n"],"mappings":";;;;;AAUO,IAAM,iBAAN,MAAkD;AAAA,EAVzD,OAUyD;AAAA;AAAA;AAAA,EAC5C,OAAe;AAAA,EAEhB,YAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EAER,YAAY,UAAiD,CAAC,GAAG;AAC7D,SAAK,aAAa,QAAQ,SAAS;AACnC,SAAK,YAAY,QAAQ,YAAY;AAAA,EACzC;AAAA,EAEQ,mBAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhD,MAAc,eAA6B;AACvC,QAAI,KAAK,UAAW,QAAO,KAAK;AAChC,QAAI,KAAK,iBAAkB,QAAO,KAAK;AAEvC,SAAK,oBAAoB,YAAY;AACjC,YAAM,EAAE,UAAU,IAAI,IAAI,MAAM,OAAO,sBAA6B;AACpE,UAAI,WAAW,KAAK;AACpB,UAAI,mBAAmB;AAEvB,WAAK,YAAY,MAAM,SAAS,sBAAsB,KAAK,YAAY;AAAA,QACnE,WAAW;AAAA,MACf,CAAC;AAED,aAAO,KAAK;AAAA,IAChB,GAAG;AAEH,QAAI;AACA,aAAO,MAAM,KAAK;AAAA,IACtB,UAAE;AACE,WAAK,mBAAmB;AAAA,IAC5B;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,MAAqC;AAC7C,UAAM,OAAO,MAAM,KAAK,aAAa;AACrC,UAAM,SAAS,MAAM,KAAK,MAAM,EAAE,SAAS,QAAQ,WAAW,KAAK,CAAC;AACpE,WAAO,OAAO;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAW,OAA0C;AACvD,QAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,UAAM,aAAa;AACnB,UAAM,OAAO,MAAM,KAAK,aAAa;AACrC,UAAM,UAA0B,CAAC;AAEjC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,YAAY;AAC/C,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,UAAU;AAC3C,YAAM,SAAS,MAAM,KAAK,OAAO,EAAE,SAAS,QAAQ,WAAW,KAAK,CAAC;AAIrE,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACnC,cAAM,QAAQ,IAAI,KAAK;AACvB,gBAAQ,KAAK,OAAO,KAAK,MAAM,OAAO,QAAQ,KAAK,IAAI,CAAiB;AAAA,MAC5E;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,QAAuB;AACzB,SAAK,YAAY;AAAA,EACrB;AACJ;","names":[]}
@@ -229,4 +229,4 @@ export {
229
229
  NoteStore,
230
230
  notes
231
231
  };
232
- //# sourceMappingURL=chunk-HNPABX7L.js.map
232
+ //# sourceMappingURL=chunk-7EZR47JV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/indexers/notes/note-store.ts","../src/indexers/notes/notes-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Note Memory Store\n * \n * Stores structured note digests for long-term agent memory.\n * Each digest captures decisions, files changed, patterns, and open questions.\n * Supports vector + BM25 hybrid retrieval via HNSW + FTS5.\n * \n * Memory tiers:\n * - \"short\" (default): Full digest, last ~20 notes\n * - \"long\": Compressed to patterns + decisions only\n */\n\nimport type { Database } from '@/db/database.ts';\nimport { vecToBuffer } from '@/lib/math.ts';\nimport type { EmbeddingProvider, SearchResult } from '@/types.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport { reciprocalRankFusion } from '@/lib/rrf.ts';\nimport { sanitizeFTS, normalizeBM25 } from '@/lib/fts.ts';\n\nexport interface NoteDigest {\n title: string;\n summary: string;\n decisions?: string[];\n filesChanged?: string[];\n patterns?: string[];\n openQuestions?: string[];\n tags?: string[];\n}\n\nexport interface StoredNote extends NoteDigest {\n id: number;\n tier: 'short' | 'long';\n createdAt: number;\n score?: number;\n}\n\nexport interface RecallOptions {\n /** Max results. Default: 5 */\n k?: number;\n /** Search mode. Default: 'hybrid' */\n mode?: 'hybrid' | 'vector' | 'keyword';\n /** Minimum score threshold. Default: 0.15 */\n minScore?: number;\n /** Filter by tier. Default: all */\n tier?: 'short' | 'long';\n}\n\nexport class NoteStore {\n private _db: Database;\n private _embedding: EmbeddingProvider;\n private _hnsw: HNSWIndex;\n private _vecs: Map<number, Float32Array>;\n\n constructor(\n db: Database,\n embedding: EmbeddingProvider,\n hnsw: HNSWIndex,\n vecs: Map<number, Float32Array>,\n ) {\n this._db = db;\n this._embedding = embedding;\n this._hnsw = hnsw;\n this._vecs = vecs;\n }\n\n /**\n * Store a note digest.\n * Embeds title + summary for vector search, auto-indexed in FTS5.\n */\n async remember(digest: NoteDigest): Promise<number> {\n const { title, summary, decisions = [], filesChanged = [], patterns = [], openQuestions = [], tags = [] } = digest;\n\n // Store in SQLite\n const result = this._db.prepare(`\n INSERT INTO note_memories (title, summary, decisions_json, files_json, patterns_json, open_json, tags_json)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(\n title,\n summary,\n JSON.stringify(decisions),\n JSON.stringify(filesChanged),\n JSON.stringify(patterns),\n JSON.stringify(openQuestions),\n JSON.stringify(tags),\n );\n\n const id = Number(result.lastInsertRowid);\n\n // Embed and index\n const text = `${title}\\n${summary}\\n${decisions.join('. ')}\\n${patterns.join('. ')}`;\n const vec = await this._embedding.embed(text);\n\n this._db.prepare('INSERT INTO note_vectors (note_id, embedding) VALUES (?, ?)').run(\n id, vecToBuffer(vec),\n );\n\n this._hnsw.add(vec, id);\n this._vecs.set(id, vec);\n\n return id;\n }\n\n /**\n * Recall relevant notes.\n * Supports vector, keyword, or hybrid (default) retrieval.\n */\n async recall(query: string, options: RecallOptions = {}): Promise<StoredNote[]> {\n const { k = 5, mode = 'hybrid', minScore = 0.15, tier } = options;\n\n let results: StoredNote[];\n\n if (mode === 'keyword') {\n results = this._searchBM25(query, k);\n } else if (mode === 'vector') {\n results = await this._searchVector(query, k);\n } else {\n // Hybrid: vector + BM25 → RRF\n const [vectorHits, bm25Hits] = await Promise.all([\n this._searchVector(query, k),\n Promise.resolve(this._searchBM25(query, k)),\n ]);\n\n const fusedResults = reciprocalRankFusion(\n [\n vectorHits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n bm25Hits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n ],\n );\n\n // Map back to full StoredNote objects\n const allById = new Map<number, StoredNote>();\n for (const m of [...vectorHits, ...bm25Hits]) allById.set(m.id, m);\n\n results = fusedResults\n .map(r => {\n const mem = allById.get((r.metadata as any).id);\n if (!mem) return null;\n return { ...mem, score: r.score };\n })\n .filter(Boolean) as StoredNote[];\n }\n\n // Apply filters\n return results\n .filter(m => (m.score ?? 0) >= minScore)\n .filter(m => !tier || m.tier === tier)\n .slice(0, k);\n }\n\n /**\n * List recent notes.\n */\n list(limit: number = 20, tier?: 'short' | 'long'): StoredNote[] {\n const sql = tier\n ? 'SELECT * FROM note_memories WHERE tier = ? ORDER BY id DESC LIMIT ?'\n : 'SELECT * FROM note_memories ORDER BY id DESC LIMIT ?';\n\n const rows = tier\n ? this._db.prepare(sql).all(tier, limit) as any[]\n : this._db.prepare(sql).all(limit) as any[];\n\n return rows.map(r => this._rowToNote(r));\n }\n\n /**\n * Get total count of notes.\n */\n count(): { total: number; short: number; long: number } {\n const total = (this._db.prepare('SELECT COUNT(*) as n FROM note_memories').get() as any).n;\n const short = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'short'\").get() as any).n;\n const long = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'long'\").get() as any).n;\n return { total, short, long };\n }\n\n /**\n * Consolidate old short-term notes into long-term.\n * Keeps the most recent `keepRecent` as short-term, compresses the rest.\n */\n consolidate(keepRecent: number = 20): { promoted: number } {\n // Find short-term notes beyond the keep window\n const old = this._db.prepare(`\n SELECT id FROM note_memories \n WHERE tier = 'short' \n ORDER BY created_at DESC \n LIMIT -1 OFFSET ?\n `).all(keepRecent) as any[];\n\n if (old.length === 0) return { promoted: 0 };\n\n const ids = old.map((r: any) => r.id);\n const placeholders = ids.map(() => '?').join(',');\n\n // Promote to long-term: clear verbose fields, keep patterns + decisions\n this._db.prepare(`\n UPDATE note_memories \n SET tier = 'long',\n open_json = '[]',\n files_json = '[]'\n WHERE id IN (${placeholders})\n `).run(...ids);\n\n return { promoted: ids.length };\n }\n\n // ── Private helpers ────────────────────────────\n\n private async _searchVector(query: string, k: number): Promise<StoredNote[]> {\n if (this._hnsw.size === 0) return [];\n\n const queryVec = await this._embedding.embed(query);\n const hits = this._hnsw.search(queryVec, k);\n\n if (hits.length === 0) return [];\n\n const ids = hits.map(h => h.id);\n const scoreMap = new Map(hits.map(h => [h.id, h.score]));\n const placeholders = ids.map(() => '?').join(',');\n\n const rows = this._db.prepare(\n `SELECT * FROM note_memories WHERE id IN (${placeholders})`\n ).all(...ids) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: scoreMap.get(r.id) ?? 0,\n }));\n }\n\n private _searchBM25(query: string, k: number): StoredNote[] {\n const ftsQuery = sanitizeFTS(query);\n if (!ftsQuery) return [];\n\n try {\n const rows = this._db.prepare(`\n SELECT m.*, bm25(fts_notes, 5.0, 3.0, 2.0, 2.0, 1.0) AS score\n FROM fts_notes f\n JOIN note_memories m ON m.id = f.rowid\n WHERE fts_notes MATCH ?\n ORDER BY score ASC\n LIMIT ?\n `).all(ftsQuery, k) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: normalizeBM25(r.score),\n }));\n } catch {\n return [];\n }\n }\n\n private _rowToNote(r: any): StoredNote {\n return {\n id: r.id,\n title: r.title,\n summary: r.summary,\n decisions: JSON.parse(r.decisions_json || '[]'),\n filesChanged: JSON.parse(r.files_json || '[]'),\n patterns: JSON.parse(r.patterns_json || '[]'),\n openQuestions: JSON.parse(r.open_json || '[]'),\n tags: JSON.parse(r.tags_json || '[]'),\n tier: r.tier,\n createdAt: r.created_at,\n };\n }\n}\n","/**\n * BrainBank — Notes Module\n * \n * Store structured conversation digests so the agent\n * remembers past discussions.\n * \n * import { notes } from 'brainbank/notes';\n * brain.use(notes());\n */\n\nimport type { Indexer, IndexerContext } from '@/indexers/base.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport { NoteStore } from './note-store.ts';\nimport type { NoteDigest, StoredNote, RecallOptions } from './note-store.ts';\n\nclass NotesPlugin implements Indexer {\n readonly name = 'notes';\n hnsw!: HNSWIndex;\n store!: NoteStore;\n vecCache = new Map<number, Float32Array>();\n\n async initialize(ctx: IndexerContext): Promise<void> {\n this.hnsw = await ctx.createHnsw(100_000);\n ctx.loadVectors('note_vectors', 'note_id', this.hnsw, this.vecCache);\n this.store = new NoteStore(ctx.db, ctx.embedding, this.hnsw, this.vecCache);\n }\n\n /** Store a note digest. */\n async remember(digest: NoteDigest): Promise<number> {\n return this.store.remember(digest);\n }\n\n /** Recall relevant notes (hybrid search). */\n async recall(query: string, options?: RecallOptions): Promise<StoredNote[]> {\n return this.store.recall(query, options);\n }\n\n /** List recent notes. */\n list(limit?: number, tier?: 'short' | 'long'): StoredNote[] {\n return this.store.list(limit, tier);\n }\n\n /** Consolidate old short-term → long-term. */\n consolidate(keepRecent?: number): { promoted: number } {\n return this.store.consolidate(keepRecent);\n }\n\n /** Count notes by tier. */\n count(): { total: number; short: number; long: number } {\n return this.store.count();\n }\n\n stats(): Record<string, any> {\n return this.store.count();\n }\n}\n\n/** Create a notes plugin. */\nexport function notes(): Indexer {\n return new NotesPlugin();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/indexers/notes/note-store.ts","../src/indexers/notes/notes-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Note Memory Store\n * \n * Stores structured note digests for long-term agent memory.\n * Each digest captures decisions, files changed, patterns, and open questions.\n * Supports vector + BM25 hybrid retrieval via HNSW + FTS5.\n * \n * Memory tiers:\n * - \"short\" (default): Full digest, last ~20 notes\n * - \"long\": Compressed to patterns + decisions only\n */\n\nimport type { Database } from '@/db/database.ts';\nimport { vecToBuffer } from '@/lib/math.ts';\nimport type { EmbeddingProvider, SearchResult } from '@/types.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport { reciprocalRankFusion } from '@/lib/rrf.ts';\nimport { sanitizeFTS, normalizeBM25 } from '@/lib/fts.ts';\n\nexport interface NoteDigest {\n title: string;\n summary: string;\n decisions?: string[];\n filesChanged?: string[];\n patterns?: string[];\n openQuestions?: string[];\n tags?: string[];\n}\n\nexport interface StoredNote extends NoteDigest {\n id: number;\n tier: 'short' | 'long';\n createdAt: number;\n score?: number;\n}\n\nexport interface RecallOptions {\n /** Max results. Default: 5 */\n k?: number;\n /** Search mode. Default: 'hybrid' */\n mode?: 'hybrid' | 'vector' | 'keyword';\n /** Minimum score threshold. Default: 0.15 */\n minScore?: number;\n /** Filter by tier. Default: all */\n tier?: 'short' | 'long';\n}\n\nexport class NoteStore {\n private _db: Database;\n private _embedding: EmbeddingProvider;\n private _hnsw: HNSWIndex;\n private _vecs: Map<number, Float32Array>;\n\n constructor(\n db: Database,\n embedding: EmbeddingProvider,\n hnsw: HNSWIndex,\n vecs: Map<number, Float32Array>,\n ) {\n this._db = db;\n this._embedding = embedding;\n this._hnsw = hnsw;\n this._vecs = vecs;\n }\n\n /**\n * Store a note digest.\n * Embeds title + summary for vector search, auto-indexed in FTS5.\n */\n async remember(digest: NoteDigest): Promise<number> {\n const { title, summary, decisions = [], filesChanged = [], patterns = [], openQuestions = [], tags = [] } = digest;\n\n // Store in SQLite\n const result = this._db.prepare(`\n INSERT INTO note_memories (title, summary, decisions_json, files_json, patterns_json, open_json, tags_json)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(\n title,\n summary,\n JSON.stringify(decisions),\n JSON.stringify(filesChanged),\n JSON.stringify(patterns),\n JSON.stringify(openQuestions),\n JSON.stringify(tags),\n );\n\n const id = Number(result.lastInsertRowid);\n\n // Embed and index\n const text = `${title}\\n${summary}\\n${decisions.join('. ')}\\n${patterns.join('. ')}`;\n const vec = await this._embedding.embed(text);\n\n this._db.prepare('INSERT INTO note_vectors (note_id, embedding) VALUES (?, ?)').run(\n id, vecToBuffer(vec),\n );\n\n this._hnsw.add(vec, id);\n this._vecs.set(id, vec);\n\n return id;\n }\n\n /**\n * Recall relevant notes.\n * Supports vector, keyword, or hybrid (default) retrieval.\n */\n async recall(query: string, options: RecallOptions = {}): Promise<StoredNote[]> {\n const { k = 5, mode = 'hybrid', minScore = 0.15, tier } = options;\n\n let results: StoredNote[];\n\n if (mode === 'keyword') {\n results = this._searchBM25(query, k);\n } else if (mode === 'vector') {\n results = await this._searchVector(query, k);\n } else {\n // Hybrid: vector + BM25 → RRF\n const [vectorHits, bm25Hits] = await Promise.all([\n this._searchVector(query, k),\n Promise.resolve(this._searchBM25(query, k)),\n ]);\n\n const fusedResults = reciprocalRankFusion(\n [\n vectorHits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n bm25Hits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n ],\n );\n\n // Map back to full StoredNote objects\n const allById = new Map<number, StoredNote>();\n for (const m of [...vectorHits, ...bm25Hits]) allById.set(m.id, m);\n\n results = fusedResults\n .map(r => {\n const mem = allById.get((r.metadata as any).id);\n if (!mem) return null;\n return { ...mem, score: r.score };\n })\n .filter(Boolean) as StoredNote[];\n }\n\n // Apply filters\n return results\n .filter(m => (m.score ?? 0) >= minScore)\n .filter(m => !tier || m.tier === tier)\n .slice(0, k);\n }\n\n /**\n * List recent notes.\n */\n list(limit: number = 20, tier?: 'short' | 'long'): StoredNote[] {\n const sql = tier\n ? 'SELECT * FROM note_memories WHERE tier = ? ORDER BY id DESC LIMIT ?'\n : 'SELECT * FROM note_memories ORDER BY id DESC LIMIT ?';\n\n const rows = tier\n ? this._db.prepare(sql).all(tier, limit) as any[]\n : this._db.prepare(sql).all(limit) as any[];\n\n return rows.map(r => this._rowToNote(r));\n }\n\n /**\n * Get total count of notes.\n */\n count(): { total: number; short: number; long: number } {\n const total = (this._db.prepare('SELECT COUNT(*) as n FROM note_memories').get() as any).n;\n const short = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'short'\").get() as any).n;\n const long = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'long'\").get() as any).n;\n return { total, short, long };\n }\n\n /**\n * Consolidate old short-term notes into long-term.\n * Keeps the most recent `keepRecent` as short-term, compresses the rest.\n */\n consolidate(keepRecent: number = 20): { promoted: number } {\n // Find short-term notes beyond the keep window\n const old = this._db.prepare(`\n SELECT id FROM note_memories \n WHERE tier = 'short' \n ORDER BY created_at DESC \n LIMIT -1 OFFSET ?\n `).all(keepRecent) as any[];\n\n if (old.length === 0) return { promoted: 0 };\n\n const ids = old.map((r: any) => r.id);\n const placeholders = ids.map(() => '?').join(',');\n\n // Promote to long-term: clear verbose fields, keep patterns + decisions\n this._db.prepare(`\n UPDATE note_memories \n SET tier = 'long',\n open_json = '[]',\n files_json = '[]'\n WHERE id IN (${placeholders})\n `).run(...ids);\n\n return { promoted: ids.length };\n }\n\n // ── Private helpers ────────────────────────────\n\n private async _searchVector(query: string, k: number): Promise<StoredNote[]> {\n if (this._hnsw.size === 0) return [];\n\n const queryVec = await this._embedding.embed(query);\n const hits = this._hnsw.search(queryVec, k);\n\n if (hits.length === 0) return [];\n\n const ids = hits.map(h => h.id);\n const scoreMap = new Map(hits.map(h => [h.id, h.score]));\n const placeholders = ids.map(() => '?').join(',');\n\n const rows = this._db.prepare(\n `SELECT * FROM note_memories WHERE id IN (${placeholders})`\n ).all(...ids) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: scoreMap.get(r.id) ?? 0,\n }));\n }\n\n private _searchBM25(query: string, k: number): StoredNote[] {\n const ftsQuery = sanitizeFTS(query);\n if (!ftsQuery) return [];\n\n try {\n const rows = this._db.prepare(`\n SELECT m.*, bm25(fts_notes, 5.0, 3.0, 2.0, 2.0, 1.0) AS score\n FROM fts_notes f\n JOIN note_memories m ON m.id = f.rowid\n WHERE fts_notes MATCH ?\n ORDER BY score ASC\n LIMIT ?\n `).all(ftsQuery, k) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: normalizeBM25(r.score),\n }));\n } catch {\n return [];\n }\n }\n\n private _rowToNote(r: any): StoredNote {\n return {\n id: r.id,\n title: r.title,\n summary: r.summary,\n decisions: JSON.parse(r.decisions_json || '[]'),\n filesChanged: JSON.parse(r.files_json || '[]'),\n patterns: JSON.parse(r.patterns_json || '[]'),\n openQuestions: JSON.parse(r.open_json || '[]'),\n tags: JSON.parse(r.tags_json || '[]'),\n tier: r.tier,\n createdAt: r.created_at,\n };\n }\n}\n","/**\n * BrainBank — Notes Module\n * \n * Store structured conversation digests so the agent\n * remembers past discussions.\n * \n * import { notes } from 'brainbank/notes';\n * brain.use(notes());\n */\n\nimport type { Plugin, PluginContext } from '@/indexers/base.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport { NoteStore } from './note-store.ts';\nimport type { NoteDigest, StoredNote, RecallOptions } from './note-store.ts';\n\nclass NotesPlugin implements Plugin {\n readonly name = 'notes';\n hnsw!: HNSWIndex;\n store!: NoteStore;\n vecCache = new Map<number, Float32Array>();\n\n async initialize(ctx: PluginContext): Promise<void> {\n this.hnsw = await ctx.createHnsw(100_000);\n ctx.loadVectors('note_vectors', 'note_id', this.hnsw, this.vecCache);\n this.store = new NoteStore(ctx.db, ctx.embedding, this.hnsw, this.vecCache);\n }\n\n /** Store a note digest. */\n async remember(digest: NoteDigest): Promise<number> {\n return this.store.remember(digest);\n }\n\n /** Recall relevant notes (hybrid search). */\n async recall(query: string, options?: RecallOptions): Promise<StoredNote[]> {\n return this.store.recall(query, options);\n }\n\n /** List recent notes. */\n list(limit?: number, tier?: 'short' | 'long'): StoredNote[] {\n return this.store.list(limit, tier);\n }\n\n /** Consolidate old short-term → long-term. */\n consolidate(keepRecent?: number): { promoted: number } {\n return this.store.consolidate(keepRecent);\n }\n\n /** Count notes by tier. */\n count(): { total: number; short: number; long: number } {\n return this.store.count();\n }\n\n stats(): Record<string, any> {\n return this.store.count();\n }\n}\n\n/** Create a notes plugin. */\nexport function notes(): Plugin {\n return new NotesPlugin();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,41 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/providers/embeddings/resolve.ts
6
+ async function resolveEmbedding(key) {
7
+ switch (key) {
8
+ case "openai": {
9
+ const { OpenAIEmbedding } = await import("./openai-embedding-VQZCZQYT.js");
10
+ return new OpenAIEmbedding();
11
+ }
12
+ case "perplexity": {
13
+ const { PerplexityEmbedding } = await import("./perplexity-embedding-227WQY4R.js");
14
+ return new PerplexityEmbedding();
15
+ }
16
+ case "perplexity-context": {
17
+ const { PerplexityContextEmbedding } = await import("./perplexity-context-embedding-KSVSZXMD.js");
18
+ return new PerplexityContextEmbedding();
19
+ }
20
+ case "local":
21
+ default: {
22
+ const { LocalEmbedding } = await import("./local-embedding-ZIMTK6PU.js");
23
+ return new LocalEmbedding();
24
+ }
25
+ }
26
+ }
27
+ __name(resolveEmbedding, "resolveEmbedding");
28
+ function providerKey(p) {
29
+ const name = p.constructor?.name ?? "";
30
+ if (name === "OpenAIEmbedding") return "openai";
31
+ if (name === "PerplexityEmbedding") return "perplexity";
32
+ if (name === "PerplexityContextEmbedding") return "perplexity-context";
33
+ return "local";
34
+ }
35
+ __name(providerKey, "providerKey");
36
+
37
+ export {
38
+ resolveEmbedding,
39
+ providerKey
40
+ };
41
+ //# sourceMappingURL=chunk-B77KABWH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/embeddings/resolve.ts"],"sourcesContent":["/**\n * BrainBank — Embedding Provider Resolver\n *\n * Resolves an EmbeddingProvider from a stored key string.\n * Used by the Initializer to auto-resolve from DB config.\n */\n\nimport type { EmbeddingProvider } from '@/types.ts';\n\n/** Known embedding provider keys. */\nexport type EmbeddingKey = 'local' | 'openai' | 'perplexity' | 'perplexity-context';\n\n/** Resolve an EmbeddingProvider from a key string. Lazy-loads the provider module. */\nexport async function resolveEmbedding(key: string): Promise<EmbeddingProvider> {\n switch (key) {\n case 'openai': {\n const { OpenAIEmbedding } = await import('./openai-embedding.ts');\n return new OpenAIEmbedding();\n }\n case 'perplexity': {\n const { PerplexityEmbedding } = await import('./perplexity-embedding.ts');\n return new PerplexityEmbedding();\n }\n case 'perplexity-context': {\n const { PerplexityContextEmbedding } = await import('./perplexity-context-embedding.ts');\n return new PerplexityContextEmbedding();\n }\n case 'local':\n default: {\n const { LocalEmbedding } = await import('./local-embedding.ts');\n return new LocalEmbedding();\n }\n }\n}\n\n/** Infer a stable key from an existing provider instance. */\nexport function providerKey(p: EmbeddingProvider): EmbeddingKey {\n const name = p.constructor?.name ?? '';\n if (name === 'OpenAIEmbedding') return 'openai';\n if (name === 'PerplexityEmbedding') return 'perplexity';\n if (name === 'PerplexityContextEmbedding') return 'perplexity-context';\n return 'local';\n}\n"],"mappings":";;;;;AAaA,eAAsB,iBAAiB,KAAyC;AAC5E,UAAQ,KAAK;AAAA,IACT,KAAK,UAAU;AACX,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,gCAAuB;AAChE,aAAO,IAAI,gBAAgB;AAAA,IAC/B;AAAA,IACA,KAAK,cAAc;AACf,YAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,oCAA2B;AACxE,aAAO,IAAI,oBAAoB;AAAA,IACnC;AAAA,IACA,KAAK,sBAAsB;AACvB,YAAM,EAAE,2BAA2B,IAAI,MAAM,OAAO,4CAAmC;AACvF,aAAO,IAAI,2BAA2B;AAAA,IAC1C;AAAA,IACA,KAAK;AAAA,IACL,SAAS;AACL,YAAM,EAAE,eAAe,IAAI,MAAM,OAAO,+BAAsB;AAC9D,aAAO,IAAI,eAAe;AAAA,IAC9B;AAAA,EACJ;AACJ;AApBsB;AAuBf,SAAS,YAAY,GAAoC;AAC5D,QAAM,OAAO,EAAE,aAAa,QAAQ;AACpC,MAAI,SAAS,kBAAmB,QAAO;AACvC,MAAI,SAAS,sBAAuB,QAAO;AAC3C,MAAI,SAAS,6BAA8B,QAAO;AAClD,SAAO;AACX;AANgB;","names":[]}