brainbank 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/dist/{base-9vfWRHCV.d.ts → base-4SUgeRWT.d.ts} +25 -2
- package/dist/{chunk-ZJ5LLMGM.js → chunk-2BEWWQL2.js} +432 -415
- package/dist/chunk-2BEWWQL2.js.map +1 -0
- package/dist/{chunk-OPQ3ZIPV.js → chunk-5VUYPNH3.js} +47 -3
- package/dist/chunk-5VUYPNH3.js.map +1 -0
- package/dist/chunk-CCXVL56V.js +120 -0
- package/dist/chunk-CCXVL56V.js.map +1 -0
- package/dist/{chunk-FEAMUZGJ.js → chunk-E6WQM4DN.js} +3 -3
- package/dist/chunk-E6WQM4DN.js.map +1 -0
- package/dist/chunk-FI7GWG4W.js +309 -0
- package/dist/chunk-FI7GWG4W.js.map +1 -0
- package/dist/{chunk-X6645UVR.js → chunk-FINIFKAY.js} +136 -4
- package/dist/chunk-FINIFKAY.js.map +1 -0
- package/dist/{chunk-4DM3XWO6.js → chunk-MGIFEPYZ.js} +54 -42
- package/dist/chunk-MGIFEPYZ.js.map +1 -0
- package/dist/{chunk-T2VXF5S5.js → chunk-Y3JKI6QN.js} +152 -137
- package/dist/chunk-Y3JKI6QN.js.map +1 -0
- package/dist/cli.js +52 -43
- package/dist/cli.js.map +1 -1
- package/dist/code.d.ts +1 -1
- package/dist/code.js +1 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.js +1 -1
- package/dist/index.d.ts +121 -82
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +1 -1
- package/dist/memory.js +3 -137
- package/dist/memory.js.map +1 -1
- package/dist/notes.d.ts +1 -1
- package/dist/notes.js +4 -49
- package/dist/notes.js.map +1 -1
- package/dist/{openai-CYDMYX7X.js → openai-embedding-VQZCZQYT.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-4DM3XWO6.js.map +0 -1
- package/dist/chunk-7JCEW7LT.js +0 -266
- package/dist/chunk-7JCEW7LT.js.map +0 -1
- package/dist/chunk-FEAMUZGJ.js.map +0 -1
- package/dist/chunk-GUT5MSJT.js +0 -99
- package/dist/chunk-GUT5MSJT.js.map +0 -1
- package/dist/chunk-OPQ3ZIPV.js.map +0 -1
- package/dist/chunk-T2VXF5S5.js.map +0 -1
- package/dist/chunk-X6645UVR.js.map +0 -1
- package/dist/chunk-ZJ5LLMGM.js.map +0 -1
- /package/dist/{openai-CYDMYX7X.js.map → openai-embedding-VQZCZQYT.js.map} +0 -0
package/dist/chunk-GUT5MSJT.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__name
|
|
3
|
-
} from "./chunk-7QVYU63E.js";
|
|
4
|
-
|
|
5
|
-
// src/providers/embeddings/openai.ts
|
|
6
|
-
var DEFAULT_MODEL = "text-embedding-3-small";
|
|
7
|
-
var DEFAULT_DIMS = {
|
|
8
|
-
"text-embedding-3-small": 1536,
|
|
9
|
-
"text-embedding-3-large": 3072,
|
|
10
|
-
"text-embedding-ada-002": 1536
|
|
11
|
-
};
|
|
12
|
-
var API_URL = "https://api.openai.com/v1/embeddings";
|
|
13
|
-
var MAX_BATCH = 100;
|
|
14
|
-
var OpenAIEmbedding = class {
|
|
15
|
-
static {
|
|
16
|
-
__name(this, "OpenAIEmbedding");
|
|
17
|
-
}
|
|
18
|
-
dims;
|
|
19
|
-
_apiKey;
|
|
20
|
-
_model;
|
|
21
|
-
_baseUrl;
|
|
22
|
-
_requestDims;
|
|
23
|
-
constructor(options = {}) {
|
|
24
|
-
this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
|
|
25
|
-
this._model = options.model ?? DEFAULT_MODEL;
|
|
26
|
-
this._baseUrl = options.baseUrl ?? API_URL;
|
|
27
|
-
if (options.dims && this._model.startsWith("text-embedding-3")) {
|
|
28
|
-
this._requestDims = options.dims;
|
|
29
|
-
this.dims = options.dims;
|
|
30
|
-
} else {
|
|
31
|
-
this.dims = options.dims ?? DEFAULT_DIMS[this._model] ?? 1536;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async embed(text) {
|
|
35
|
-
const results = await this._request([text]);
|
|
36
|
-
return results[0];
|
|
37
|
-
}
|
|
38
|
-
async embedBatch(texts) {
|
|
39
|
-
if (texts.length === 0) return [];
|
|
40
|
-
const results = [];
|
|
41
|
-
for (let i = 0; i < texts.length; i += MAX_BATCH) {
|
|
42
|
-
const batch = texts.slice(i, i + MAX_BATCH);
|
|
43
|
-
const embeddings = await this._request(batch);
|
|
44
|
-
results.push(...embeddings);
|
|
45
|
-
}
|
|
46
|
-
return results;
|
|
47
|
-
}
|
|
48
|
-
async close() {
|
|
49
|
-
}
|
|
50
|
-
_isTokenLimitError(errText) {
|
|
51
|
-
return errText.includes("maximum input length") || errText.includes("maximum context length") || errText.includes("too many tokens");
|
|
52
|
-
}
|
|
53
|
-
async _request(input, retryDepth = 0) {
|
|
54
|
-
if (!this._apiKey) {
|
|
55
|
-
throw new Error("OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.");
|
|
56
|
-
}
|
|
57
|
-
const MAX_CHARS = 24e3;
|
|
58
|
-
const safeInput = input.map((t) => t.length > MAX_CHARS ? t.slice(0, MAX_CHARS) : t);
|
|
59
|
-
const body = {
|
|
60
|
-
model: this._model,
|
|
61
|
-
input: safeInput
|
|
62
|
-
};
|
|
63
|
-
if (this._requestDims) {
|
|
64
|
-
body.dimensions = this._requestDims;
|
|
65
|
-
}
|
|
66
|
-
const res = await fetch(this._baseUrl, {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: {
|
|
69
|
-
"Content-Type": "application/json",
|
|
70
|
-
"Authorization": `Bearer ${this._apiKey}`
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify(body)
|
|
73
|
-
});
|
|
74
|
-
if (!res.ok) {
|
|
75
|
-
const err = await res.text();
|
|
76
|
-
const isTokenLimit = res.status === 400 && this._isTokenLimitError(err);
|
|
77
|
-
if (isTokenLimit && safeInput.length > 1) {
|
|
78
|
-
const results = [];
|
|
79
|
-
for (const text of safeInput) {
|
|
80
|
-
const r = await this._request([text.slice(0, 8e3)]);
|
|
81
|
-
results.push(r[0]);
|
|
82
|
-
}
|
|
83
|
-
return results;
|
|
84
|
-
}
|
|
85
|
-
if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {
|
|
86
|
-
return await this._request([safeInput[0].slice(0, 6e3)], retryDepth + 1);
|
|
87
|
-
}
|
|
88
|
-
throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);
|
|
89
|
-
}
|
|
90
|
-
const json = await res.json();
|
|
91
|
-
const sorted = json.data.sort((a, b) => a.index - b.index);
|
|
92
|
-
return sorted.map((d) => new Float32Array(d.embedding));
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export {
|
|
97
|
-
OpenAIEmbedding
|
|
98
|
-
};
|
|
99
|
-
//# sourceMappingURL=chunk-GUT5MSJT.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/providers/embeddings/openai.ts"],"sourcesContent":["/**\n * BrainBank — OpenAI Embedding Provider\n * \n * Uses OpenAI's embedding API via fetch (no SDK dependency).\n * Supports text-embedding-3-small, text-embedding-3-large, and ada-002.\n * \n * Usage:\n * const brain = new BrainBank({\n * embeddingProvider: new OpenAIEmbedding({ model: 'text-embedding-3-small' }),\n * });\n */\n\nimport type { EmbeddingProvider } from '../../types.ts';\n\nconst DEFAULT_MODEL = 'text-embedding-3-small';\nconst DEFAULT_DIMS: Record<string, number> = {\n 'text-embedding-3-small': 1536,\n 'text-embedding-3-large': 3072,\n 'text-embedding-ada-002': 1536,\n};\nconst API_URL = 'https://api.openai.com/v1/embeddings';\nconst MAX_BATCH = 100; // OpenAI limit per request\n\nexport interface OpenAIEmbeddingOptions {\n /** OpenAI API key. Falls back to OPENAI_API_KEY env var. */\n apiKey?: string;\n /** Model name. Default: 'text-embedding-3-small' */\n model?: string;\n /** Vector dimensions. If omitted, uses model default. text-embedding-3-* supports custom dims. */\n dims?: number;\n /** Base URL override (for Azure, proxies, etc.) */\n baseUrl?: string;\n}\n\nexport class OpenAIEmbedding implements EmbeddingProvider {\n readonly dims: number;\n\n private _apiKey: string;\n private _model: string;\n private _baseUrl: string;\n private _requestDims: number | undefined;\n\n constructor(options: OpenAIEmbeddingOptions = {}) {\n this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? '';\n this._model = options.model ?? DEFAULT_MODEL;\n this._baseUrl = options.baseUrl ?? API_URL;\n\n // Custom dims only supported by text-embedding-3-*\n if (options.dims && this._model.startsWith('text-embedding-3')) {\n this._requestDims = options.dims;\n this.dims = options.dims;\n } else {\n this.dims = options.dims ?? DEFAULT_DIMS[this._model] ?? 1536;\n }\n }\n\n async embed(text: string): Promise<Float32Array> {\n const results = await this._request([text]);\n return results[0];\n }\n\n async embedBatch(texts: string[]): Promise<Float32Array[]> {\n if (texts.length === 0) return [];\n\n const results: Float32Array[] = [];\n\n // Split into chunks of MAX_BATCH\n for (let i = 0; i < texts.length; i += MAX_BATCH) {\n const batch = texts.slice(i, i + MAX_BATCH);\n const embeddings = await this._request(batch);\n results.push(...embeddings);\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n // No resources to release\n }\n\n private _isTokenLimitError(errText: string): boolean {\n return errText.includes('maximum input length') ||\n errText.includes('maximum context length') ||\n errText.includes('too many tokens');\n }\n\n private async _request(input: string[], retryDepth: number = 0): Promise<Float32Array[]> {\n if (!this._apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n // Truncate texts that would exceed token limit (~4 chars per token, 8192 max)\n const MAX_CHARS = 24_000;\n const safeInput = input.map(t => t.length > MAX_CHARS ? t.slice(0, MAX_CHARS) : t);\n\n const body: Record<string, any> = {\n model: this._model,\n input: safeInput,\n };\n\n if (this._requestDims) {\n body.dimensions = this._requestDims;\n }\n\n const res = await fetch(this._baseUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this._apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.text();\n const isTokenLimit = res.status === 400 && this._isTokenLimitError(err);\n\n // If token limit error in a batch, retry each item individually with more aggressive truncation\n if (isTokenLimit && safeInput.length > 1) {\n const results: Float32Array[] = [];\n for (const text of safeInput) {\n const r = await this._request([text.slice(0, 8_000)]);\n results.push(r[0]);\n }\n return results;\n }\n // Last resort: if single item still fails, truncate to ~2k tokens (max 1 retry)\n if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {\n return await this._request([safeInput[0].slice(0, 6_000)], retryDepth + 1);\n }\n throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);\n }\n\n const json = await res.json() as {\n data: Array<{ embedding: number[]; index: number }>;\n };\n\n // Sort by index (API may return out of order)\n const sorted = json.data.sort((a, b) => a.index - b.index);\n\n return sorted.map(d => new Float32Array(d.embedding));\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/indexers/notes/note-store.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 type { EmbeddingProvider, SearchResult } from '../../types.ts';\nimport type { HNSWIndex } from '../../providers/vector/hnsw.ts';\nimport { BM25Search } from '../../search/keyword/bm25.ts';\nimport { reciprocalRankFusion } from '../../search/rrf.ts';\nimport { sanitizeFTS, normalizeBM25 } from '../../search/keyword/utils.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, Buffer.from(vec.buffer),\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"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/indexers/docs/docs-indexer.ts","../src/indexers/docs/docs-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Document Indexer\n * \n * Indexes generic document collections (markdown, text, etc.)\n * with heading-aware smart chunking, inspired by qmd.\n * \n * const indexer = new DocsIndexer(db, embedding, hnsw, vecCache);\n * await indexer.indexCollection('notes', '/path/to/notes', '**\\/*.md');\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { createHash } from 'node:crypto';\n\nimport type { Database } from '../../db/database.ts';\nimport type { EmbeddingProvider, VectorIndex } from '../../types.ts';\nimport type { HNSWIndex } from '../../providers/vector/hnsw.ts';\n\n// ── Break Point Scoring (qmd-inspired) ──────────────\n\ninterface BreakPoint {\n pos: number; // character position\n score: number; // break quality (higher = better)\n}\n\nconst BREAK_SCORES: [RegExp, number][] = [\n [/^# /, 100], // H1\n [/^## /, 90], // H2\n [/^### /, 80], // H3\n [/^#### /, 70], // H4\n [/^##### /, 60], // H5\n [/^###### /, 50], // H6\n [/^```/, 80], // Code fence\n [/^---$/, 60], // Horizontal rule\n [/^\\*\\*\\*$/, 60], // Horizontal rule alt\n [/^$/, 20], // Blank line (paragraph break)\n [/^[-*+] /, 5], // List item\n];\n\n// ── Chunk Target ────────────────────────────────────\n\nconst TARGET_CHARS = 3000; // ~900 tokens\nconst WINDOW_CHARS = 600; // search window before cutoff\nconst MIN_CHUNK_CHARS = 200; // don't create tiny chunks\n\n/** Escape special regex characters so user-provided patterns behave as literals. */\nfunction escapeRegex(s: string): string {\n return s.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// ── DocsIndexer ──────────────────────────────────────\n\nexport class DocsIndexer {\n constructor(\n private _db: Database,\n private _embedding: EmbeddingProvider,\n private _hnsw: HNSWIndex,\n private _vecCache: Map<number, Float32Array>,\n ) {}\n\n /**\n * Index all documents in a collection.\n * Incremental — skips unchanged files (by content hash).\n */\n async indexCollection(\n collection: string,\n dirPath: string,\n pattern: string = '**/*.md',\n options: {\n ignore?: string[];\n onProgress?: (file: string, current: number, total: number) => void;\n } = {},\n ): Promise<{ indexed: number; skipped: number; chunks: number }> {\n // Resolve absolute path\n const absDir = path.resolve(dirPath);\n if (!fs.existsSync(absDir)) {\n throw new Error(`Collection path does not exist: ${absDir}`);\n }\n\n // Find files matching pattern\n const patternExt = pattern.match(/\\.([\\w]+)$/)?.[1];\n const files: string[] = [];\n\n const walkDir = (dir: string, base: string): void => {\n let entries: fs.Dirent[];\n try { entries = fs.readdirSync(dir, { withFileTypes: true }); }\n catch { return; }\n for (const e of entries) {\n const rel = base ? `${base}/${e.name}` : e.name;\n if (e.isDirectory()) {\n // Skip ignored directories (node_modules, .git, etc.)\n if (this._isIgnoredDocDir(e.name)) continue;\n walkDir(path.join(dir, e.name), rel);\n } else if (e.isFile()) {\n const shouldIgnore = options.ignore?.some(ig => {\n // Convert glob to regex: ** → .*, * → [^/]*, escape the rest\n const regex = ig\n .replace(/\\*\\*/g, '{{GLOBSTAR}}')\n .replace(/\\*/g, '{{STAR}}')\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\{\\{GLOBSTAR\\}\\}/g, '.*')\n .replace(/\\{\\{STAR\\}\\}/g, '[^/]*');\n return new RegExp(regex).test(rel);\n });\n const ext = path.extname(e.name).slice(1);\n if (!shouldIgnore && (!patternExt || ext === patternExt)) {\n files.push(rel);\n }\n }\n }\n };\n walkDir(absDir, '');\n\n let indexed = 0;\n let skipped = 0;\n let totalChunks = 0;\n\n for (let i = 0; i < files.length; i++) {\n const relPath = files[i];\n const absPath = path.join(absDir, relPath);\n\n options.onProgress?.(relPath, i + 1, files.length);\n\n // Read content and hash\n const content = fs.readFileSync(absPath, 'utf-8');\n const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);\n\n // Check if already indexed with same hash AND vectors exist\n const existingChunks = this._db.prepare(\n `SELECT dc.id, dc.content_hash, dv.chunk_id AS has_vector\n FROM doc_chunks dc\n LEFT JOIN doc_vectors dv ON dv.chunk_id = dc.id\n WHERE dc.collection = ? AND dc.file_path = ?`\n ).all(collection, relPath) as any[];\n\n const allMatch = existingChunks.length > 0 &&\n existingChunks.every((c: any) => c.content_hash === hash && c.has_vector != null);\n\n if (allMatch) {\n skipped++;\n continue;\n }\n\n // Remove old chunks + their HNSW vectors\n for (const old of existingChunks) {\n this._hnsw.remove(old.id);\n this._vecCache.delete(old.id);\n }\n this._db.prepare(\n 'DELETE FROM doc_chunks WHERE collection = ? AND file_path = ?'\n ).run(collection, relPath);\n\n // Extract title and chunk\n const title = this._extractTitle(content, relPath);\n const chunks = this._smartChunk(content);\n\n // Insert chunks\n const insertChunk = this._db.prepare(`\n INSERT INTO doc_chunks (collection, file_path, title, content, seq, pos, content_hash)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const chunkIds: number[] = [];\n\n this._db.transaction(() => {\n for (let seq = 0; seq < chunks.length; seq++) {\n const chunk = chunks[seq];\n const result = insertChunk.run(\n collection, relPath, title, chunk.text, seq, chunk.pos, hash,\n );\n chunkIds.push(Number(result.lastInsertRowid));\n }\n });\n\n // Generate embeddings\n const texts = chunks.map(c => `title: ${title} | text: ${c.text}`);\n const embeddings = await this._embedding.embedBatch(texts);\n\n // Store vectors — DB transaction commits first, then HNSW is updated.\n const insertVec = this._db.prepare(\n 'INSERT OR REPLACE INTO doc_vectors (chunk_id, embedding) VALUES (?, ?)'\n );\n\n this._db.transaction(() => {\n for (let j = 0; j < chunkIds.length; j++) {\n const buf = Buffer.from(embeddings[j].buffer);\n insertVec.run(chunkIds[j], buf);\n }\n });\n\n // Reached only if the transaction committed successfully — no orphan risk.\n for (let j = 0; j < chunkIds.length; j++) {\n this._hnsw.add(embeddings[j], chunkIds[j]);\n this._vecCache.set(chunkIds[j], embeddings[j]);\n }\n\n indexed++;\n totalChunks += chunks.length;\n }\n\n return { indexed, skipped, chunks: totalChunks };\n }\n\n /**\n * Remove all indexed data for a collection.\n */\n removeCollection(collection: string): void {\n // Clean HNSW entries before deleting DB rows\n const chunks = this._db.prepare(\n 'SELECT id FROM doc_chunks WHERE collection = ?'\n ).all(collection) as any[];\n for (const chunk of chunks) {\n this._hnsw.remove(chunk.id);\n this._vecCache.delete(chunk.id);\n }\n\n this._db.prepare('DELETE FROM doc_chunks WHERE collection = ?').run(collection);\n this._db.prepare('DELETE FROM collections WHERE name = ?').run(collection);\n this._db.prepare('DELETE FROM path_contexts WHERE collection = ?').run(collection);\n }\n\n // ── Smart Chunking ──────────────────────────────\n\n /**\n * Split document into chunks at natural markdown boundaries.\n * Uses heading-aware scoring like qmd.\n */\n private _smartChunk(text: string): { text: string; pos: number }[] {\n if (text.length <= TARGET_CHARS) {\n return [{ text, pos: 0 }];\n }\n\n const lines = text.split('\\n');\n const breakPoints = this._findBreakPoints(lines);\n const chunks: { text: string; pos: number }[] = [];\n\n let chunkStart = 0; // char position\n\n while (chunkStart < text.length) {\n const remaining = text.length - chunkStart;\n if (remaining <= TARGET_CHARS + WINDOW_CHARS) {\n // Last chunk — take everything\n const lastText = text.slice(chunkStart).trim();\n if (lastText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: lastText, pos: chunkStart });\n } else if (chunks.length > 0) {\n // Merge with previous chunk\n chunks[chunks.length - 1].text += '\\n' + lastText;\n } else {\n chunks.push({ text: lastText, pos: chunkStart });\n }\n break;\n }\n\n // Find best break point in window\n const targetEnd = chunkStart + TARGET_CHARS;\n const windowStart = targetEnd - WINDOW_CHARS;\n\n let bestBreak = targetEnd;\n let bestScore = 0;\n\n for (const bp of breakPoints) {\n if (bp.pos <= chunkStart) continue;\n if (bp.pos > targetEnd + WINDOW_CHARS / 2) break;\n if (bp.pos < windowStart) continue;\n\n // Score decay: prefer closer break points\n const distance = Math.abs(bp.pos - targetEnd);\n const decay = 1 - (distance / WINDOW_CHARS) ** 2 * 0.7;\n const finalScore = bp.score * decay;\n\n if (finalScore > bestScore) {\n bestScore = finalScore;\n bestBreak = bp.pos;\n }\n }\n\n const chunkText = text.slice(chunkStart, bestBreak).trim();\n if (chunkText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: chunkText, pos: chunkStart });\n }\n\n chunkStart = bestBreak;\n }\n\n return chunks;\n }\n\n /**\n * Find all potential break points in the document with scores.\n */\n private _findBreakPoints(lines: string[]): BreakPoint[] {\n const points: BreakPoint[] = [];\n let charPos = 0;\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code fences\n if (line.trimStart().startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n if (!inCodeBlock) {\n // End of code block is a good break point\n points.push({ pos: charPos + line.length + 1, score: 80 });\n }\n charPos += line.length + 1;\n continue;\n }\n\n // Skip break points inside code blocks\n if (inCodeBlock) {\n charPos += line.length + 1;\n continue;\n }\n\n // Score this line as a potential break point\n for (const [pattern, score] of BREAK_SCORES) {\n if (pattern.test(line.trim())) {\n points.push({ pos: charPos, score });\n break;\n }\n }\n\n charPos += line.length + 1;\n }\n\n return points;\n }\n\n /**\n * Extract document title from first heading or filename.\n */\n private _extractTitle(content: string, filePath: string): string {\n const match = content.match(/^#{1,3}\\s+(.+)$/m);\n if (match) return match[1].trim();\n return path.basename(filePath, path.extname(filePath));\n }\n\n /** Skip well-known output/vendor directories when walking docs. */\n private _isIgnoredDocDir(name: string): boolean {\n const IGNORED = new Set([\n 'node_modules', '.git', '.hg', '.svn',\n 'dist', 'build', 'out', 'coverage', '.next',\n '__pycache__', '.tox', '.venv', 'venv',\n 'vendor', 'target', '.cache', '.turbo',\n ]);\n return IGNORED.has(name);\n }\n}\n","/**\n * BrainBank — Docs Module\n * \n * Index any folder of markdown/text files (notes, docs, wikis).\n * Heading-aware smart chunking inspired by qmd.\n * \n * import { docs } from 'brainbank/docs';\n * brain.use(docs());\n */\n\nimport type { Indexer, IndexerContext } from '../base.ts';\nimport type { HNSWIndex } from '../../providers/vector/hnsw.ts';\nimport type { Database } from '../../db/database.ts';\nimport type { EmbeddingProvider, DocumentCollection, SearchResult } from '../../types.ts';\nimport { DocsIndexer } from './docs-indexer.ts';\n\nclass DocsPlugin implements Indexer {\n readonly name = 'docs';\n hnsw!: HNSWIndex;\n indexer!: DocsIndexer;\n vecCache = new Map<number, Float32Array>();\n private _db!: Database;\n private _embedding!: EmbeddingProvider;\n\n async initialize(ctx: IndexerContext): Promise<void> {\n this._db = ctx.db;\n this._embedding = ctx.embedding;\n this.hnsw = await ctx.createHnsw();\n ctx.loadVectors('doc_vectors', 'chunk_id', this.hnsw, this.vecCache);\n this.indexer = new DocsIndexer(ctx.db, ctx.embedding, this.hnsw, this.vecCache);\n }\n\n /** Register a document collection. */\n addCollection(collection: DocumentCollection): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO collections (name, path, pattern, ignore_json, context)\n VALUES (?, ?, ?, ?, ?)\n `).run(\n collection.name,\n collection.path,\n collection.pattern ?? '**/*.md',\n JSON.stringify(collection.ignore ?? []),\n collection.context ?? null,\n );\n }\n\n /** Remove a collection and its indexed data. */\n removeCollection(name: string): void {\n this.indexer.removeCollection(name);\n }\n\n /** List all registered collections. */\n listCollections(): DocumentCollection[] {\n return (this._db.prepare('SELECT * FROM collections').all() as any[]).map(row => ({\n name: row.name,\n path: row.path,\n pattern: row.pattern,\n ignore: JSON.parse(row.ignore_json),\n context: row.context,\n }));\n }\n\n /** Index all (or specific) collections. Incremental. */\n async indexCollections(options: {\n collections?: string[];\n onProgress?: (collection: string, file: string, current: number, total: number) => void;\n } = {}): Promise<Record<string, { indexed: number; skipped: number; chunks: number }>> {\n const allCollections = this.listCollections();\n const toIndex = options.collections\n ? allCollections.filter(c => options.collections!.includes(c.name))\n : allCollections;\n\n const results: Record<string, { indexed: number; skipped: number; chunks: number }> = {};\n\n for (const coll of toIndex) {\n results[coll.name] = await this.indexer.indexCollection(\n coll.name,\n coll.path,\n coll.pattern,\n {\n ignore: coll.ignore,\n onProgress: (file, cur, total) => options.onProgress?.(coll.name, file, cur, total),\n },\n );\n }\n\n return results;\n }\n\n /** Search documents only. */\n async search(query: string, options?: {\n collection?: string;\n k?: number;\n minScore?: number;\n }): Promise<SearchResult[]> {\n const k = options?.k ?? 8;\n const queryVec = await this._embedding.embed(query);\n\n // Over-fetch from shared HNSW when filtering by collection\n // (same pattern as collection.ts ratio scaling)\n let searchK = k;\n if (options?.collection && this.hnsw.size > 0) {\n const collectionCount = (this._db.prepare(\n 'SELECT COUNT(*) as c FROM doc_chunks WHERE collection = ?'\n ).get(options.collection) as any)?.c ?? 0;\n const totalChunks = (this._db.prepare(\n 'SELECT COUNT(*) as c FROM doc_chunks'\n ).get() as any)?.c ?? 1;\n const ratio = collectionCount > 0\n ? Math.max(3, Math.min(50, Math.ceil(totalChunks / collectionCount)))\n : 3;\n searchK = Math.min(k * ratio, this.hnsw.size);\n }\n\n const hits = this.hnsw.search(queryVec, searchK);\n\n const results: SearchResult[] = [];\n for (const hit of hits) {\n if (options?.minScore && hit.score < options.minScore) continue;\n\n const chunk = this._db.prepare(\n 'SELECT * FROM doc_chunks WHERE id = ?'\n ).get(hit.id) as any;\n\n if (!chunk) continue;\n if (options?.collection && chunk.collection !== options.collection) continue;\n\n const ctx = this._getDocContext(chunk.collection, chunk.file_path);\n\n results.push({\n type: 'document',\n score: hit.score,\n filePath: chunk.file_path,\n content: chunk.content,\n context: ctx,\n metadata: {\n collection: chunk.collection,\n title: chunk.title,\n seq: chunk.seq,\n },\n });\n\n // Stop once we have enough results\n if (results.length >= k) break;\n }\n\n return results;\n }\n\n /** Add context description for a document path. */\n addContext(collection: string, path: string, context: string): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO path_contexts (collection, path, context)\n VALUES (?, ?, ?)\n `).run(collection, path, context);\n }\n\n /** Remove context for a path. */\n removeContext(collection: string, path: string): void {\n this._db.prepare(\n 'DELETE FROM path_contexts WHERE collection = ? AND path = ?'\n ).run(collection, path);\n }\n\n /** List all context entries. */\n listContexts(): { collection: string; path: string; context: string }[] {\n return this._db.prepare('SELECT * FROM path_contexts').all() as any[];\n }\n\n stats(): Record<string, any> {\n return {\n collections: (this._db.prepare('SELECT COUNT(*) as c FROM collections').get() as any).c,\n documents: (this._db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM doc_chunks').get() as any).c,\n chunks: (this._db.prepare('SELECT COUNT(*) as c FROM doc_chunks').get() as any).c,\n hnswSize: this.hnsw.size,\n };\n }\n\n /** Resolve context for a document (checks path_contexts tree → collection context). */\n private _getDocContext(collection: string, filePath: string): string | undefined {\n const parts = filePath.split('/');\n for (let i = parts.length; i >= 0; i--) {\n const checkPath = i === 0 ? '/' : '/' + parts.slice(0, i).join('/');\n const ctx = this._db.prepare(\n 'SELECT context FROM path_contexts WHERE collection = ? AND path = ?'\n ).get(collection, checkPath) as any;\n if (ctx) return ctx.context;\n }\n\n const coll = this._db.prepare(\n 'SELECT context FROM collections WHERE name = ?'\n ).get(collection) as any;\n return coll?.context ?? undefined;\n }\n}\n\n/** Create a document collections plugin. */\nexport function docs(): Indexer {\n return new DocsPlugin();\n}\n"],"mappings":"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","names":["path"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/memory/store.ts","../src/memory/consolidator.ts"],"sourcesContent":["/**\n * BrainBank — Pattern Store (Agent Memory)\n * \n * Stores what the agent learned from past tasks.\n * Each pattern records task, approach, and success rate.\n * Searchable by semantic similarity via HNSW.\n */\n\nimport type { Database } from '../db/database.ts';\nimport type { EmbeddingProvider, LearningPattern } from '../types.ts';\nimport type { HNSWIndex } from '../providers/vector/hnsw.ts';\n\nexport interface PatternStoreDeps {\n db: Database;\n hnsw: HNSWIndex;\n vectorCache: Map<number, Float32Array>;\n embedding: EmbeddingProvider;\n}\n\nexport class PatternStore {\n private _deps: PatternStoreDeps;\n\n constructor(deps: PatternStoreDeps) {\n this._deps = deps;\n }\n\n /**\n * Store a learned pattern.\n * Returns the pattern ID.\n */\n async learn(pattern: LearningPattern): Promise<number> {\n const result = this._deps.db.prepare(`\n INSERT INTO memory_patterns (task_type, task, approach, outcome, success_rate, critique, tokens_used, latency_ms)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n pattern.taskType,\n pattern.task,\n pattern.approach,\n pattern.outcome ?? null,\n pattern.successRate,\n pattern.critique ?? null,\n pattern.tokensUsed ?? null,\n pattern.latencyMs ?? null,\n );\n\n const id = Number(result.lastInsertRowid);\n\n // Embed and store vector\n const text = `${pattern.taskType} ${pattern.task} ${pattern.approach}`;\n const vec = await this._deps.embedding.embed(text);\n\n this._deps.db.prepare(\n 'INSERT INTO memory_vectors (pattern_id, embedding) VALUES (?, ?)'\n ).run(id, Buffer.from(vec.buffer));\n\n this._deps.hnsw.add(vec, id);\n this._deps.vectorCache.set(id, vec);\n\n return id;\n }\n\n /**\n * Search for similar successful patterns.\n * Filters by minimum success rate.\n */\n async search(query: string, k: number = 4, minSuccess: number = 0.5): Promise<(LearningPattern & { score: number })[]> {\n if (this._deps.hnsw.size === 0) return [];\n\n const vec = await this._deps.embedding.embed(query);\n const hits = this._deps.hnsw.search(vec, k * 2);\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\n const placeholders = ids.map(() => '?').join(',');\n const rows = this._deps.db.prepare(\n `SELECT * FROM memory_patterns WHERE id IN (${placeholders}) AND success_rate >= ?`\n ).all(...ids, minSuccess) as any[];\n\n return rows\n .map(r => ({\n id: r.id,\n taskType: r.task_type,\n task: r.task,\n approach: r.approach,\n outcome: r.outcome,\n successRate: r.success_rate,\n critique: r.critique,\n tokensUsed: r.tokens_used,\n latencyMs: r.latency_ms,\n score: scoreMap.get(r.id) ?? 0,\n }))\n .sort((a, b) => b.score - a.score)\n .slice(0, k);\n }\n\n /**\n * Get all patterns for a specific task type.\n */\n getByTaskType(taskType: string, limit: number = 20): LearningPattern[] {\n const rows = this._deps.db.prepare(\n `SELECT * FROM memory_patterns WHERE task_type = ? ORDER BY success_rate DESC LIMIT ?`\n ).all(taskType, limit) as any[];\n\n return rows.map(r => ({\n id: r.id,\n taskType: r.task_type,\n task: r.task,\n approach: r.approach,\n outcome: r.outcome,\n successRate: r.success_rate,\n critique: r.critique,\n tokensUsed: r.tokens_used,\n latencyMs: r.latency_ms,\n }));\n }\n\n /** Total number of stored patterns. */\n get count(): number {\n return (this._deps.db.prepare('SELECT COUNT(*) as c FROM memory_patterns').get() as any).c;\n }\n}\n","/**\n * BrainBank — Consolidator\n * \n * Maintenance operations for the agent memory:\n * - prune: remove old failed patterns\n * - dedup: merge near-duplicate patterns (cosine > 0.95)\n * - consolidate: run both\n */\n\nimport type { Database } from '../db/database.ts';\nimport { cosineSimilarity } from '../lib/math.ts';\n\nexport class Consolidator {\n constructor(\n private _db: Database,\n private _vectorCache: Map<number, Float32Array>,\n ) {}\n\n /**\n * Remove old failed patterns.\n * Criteria: success_rate < 0.3 AND created > 90 days ago.\n */\n prune(maxAgeDays: number = 90, minSuccess: number = 0.3): number {\n const cutoff = Math.floor(Date.now() / 1000) - maxAgeDays * 86400;\n const result = this._db.prepare(\n 'DELETE FROM memory_patterns WHERE success_rate < ? AND created_at < ?'\n ).run(minSuccess, cutoff);\n return result.changes;\n }\n\n /**\n * Merge near-duplicate patterns.\n * Keeps the one with higher success_rate.\n * Threshold: cosine similarity > 0.95.\n */\n dedup(threshold: number = 0.95): number {\n const entries = Array.from(this._vectorCache.entries());\n const toDelete = new Set<number>();\n\n for (let i = 0; i < entries.length; i++) {\n if (toDelete.has(entries[i][0])) continue;\n\n for (let j = i + 1; j < entries.length; j++) {\n if (toDelete.has(entries[j][0])) continue;\n\n const sim = cosineSimilarity(entries[i][1], entries[j][1]);\n if (sim > threshold) {\n // Keep the one with higher success rate\n const pi = this._db.prepare(\n 'SELECT success_rate FROM memory_patterns WHERE id = ?'\n ).get(entries[i][0]) as any;\n const pj = this._db.prepare(\n 'SELECT success_rate FROM memory_patterns WHERE id = ?'\n ).get(entries[j][0]) as any;\n\n if (pi && pj) {\n const deleteId = pi.success_rate >= pj.success_rate\n ? entries[j][0]\n : entries[i][0];\n toDelete.add(deleteId);\n }\n }\n }\n }\n\n if (toDelete.size > 0) {\n const ids = Array.from(toDelete);\n const placeholders = ids.map(() => '?').join(',');\n this._db.prepare(\n `DELETE FROM memory_patterns WHERE id IN (${placeholders})`\n ).run(...ids);\n\n // Clean vector cache\n for (const id of ids) {\n this._vectorCache.delete(id);\n }\n }\n\n return toDelete.size;\n }\n\n /**\n * Run full consolidation: prune + dedup.\n */\n consolidate(): { pruned: number; deduped: number } {\n const pruned = this.prune();\n const deduped = this.dedup();\n return { pruned, deduped };\n }\n}\n"],"mappings":"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","names":[]}
|