brainbank 0.7.0 → 0.8.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.
- package/README.md +76 -1398
- package/bin/brainbank +5 -1
- package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
- package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
- package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
- package/dist/chunk-3JZIM5AU.js.map +1 -0
- package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
- package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
- package/dist/chunk-7JDCHUJV.js +89 -0
- package/dist/chunk-7JDCHUJV.js.map +1 -0
- package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
- package/dist/chunk-7T2ZCZQA.js.map +1 -0
- package/dist/chunk-E3J37GDA.js +74 -0
- package/dist/chunk-E3J37GDA.js.map +1 -0
- package/dist/chunk-JEFWMS5Z.js +217 -0
- package/dist/chunk-JEFWMS5Z.js.map +1 -0
- package/dist/chunk-JRVYSTMP.js +3256 -0
- package/dist/chunk-JRVYSTMP.js.map +1 -0
- package/dist/chunk-OLDHLOMT.js +69 -0
- package/dist/chunk-OLDHLOMT.js.map +1 -0
- package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
- package/dist/chunk-QTZNB6AK.js.map +1 -0
- package/dist/chunk-RFF7HMP6.js +109 -0
- package/dist/chunk-RFF7HMP6.js.map +1 -0
- package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
- package/dist/chunk-TD3TEFI3.js.map +1 -0
- package/dist/cli.js +1036 -341
- package/dist/cli.js.map +1 -1
- package/dist/haiku-expander-WOVJIVXD.js +8 -0
- package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
- package/dist/http-server-GIRELCCL.js +9 -0
- package/dist/index.d.ts +1774 -611
- package/dist/index.js +282 -70
- package/dist/index.js.map +1 -1
- package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
- package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
- package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
- package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
- package/dist/plugin-FF4Q34TI.js +32 -0
- package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
- package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
- package/package.json +25 -52
- package/src/brainbank.ts +620 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +171 -0
- package/src/cli/commands/daemon.ts +100 -0
- package/src/cli/commands/docs.ts +71 -0
- package/src/cli/commands/files.ts +69 -0
- package/src/cli/commands/help.ts +72 -0
- package/src/cli/commands/index.ts +282 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp.ts +13 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +365 -0
- package/src/cli/commands/search.ts +130 -0
- package/src/cli/commands/stats.ts +44 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +43 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +123 -0
- package/src/cli/factory/config-loader.ts +72 -0
- package/src/cli/factory/index.ts +65 -0
- package/src/cli/factory/plugin-loader.ts +146 -0
- package/src/cli/index.ts +63 -0
- package/src/cli/server-client.ts +135 -0
- package/src/cli/utils.ts +121 -0
- package/src/config.ts +50 -0
- package/src/constants.ts +13 -0
- package/src/db/adapter.ts +112 -0
- package/src/db/metadata.ts +130 -0
- package/src/db/migrations.ts +66 -0
- package/src/db/sqlite-adapter.ts +208 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +85 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +222 -0
- package/src/index.ts +159 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +125 -0
- package/src/lib/math.ts +87 -0
- package/src/lib/provider-key.ts +20 -0
- package/src/lib/prune.ts +71 -0
- package/src/lib/rerank.ts +33 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/plugin.ts +323 -0
- package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
- package/src/providers/embeddings/embedding-worker.ts +141 -0
- package/src/providers/embeddings/local-embedding.ts +115 -0
- package/src/providers/embeddings/openai-embedding.ts +167 -0
- package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
- package/src/providers/embeddings/perplexity-embedding.ts +165 -0
- package/src/providers/embeddings/resolve.ts +34 -0
- package/src/providers/pruners/haiku-expander.ts +152 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -0
- package/src/providers/rerankers/qwen3-reranker.ts +180 -0
- package/src/providers/vector/hnsw-index.ts +174 -0
- package/src/providers/vector/hnsw-loader.ts +129 -0
- package/src/search/bm25-boost.ts +61 -0
- package/src/search/context-builder.ts +298 -0
- package/src/search/keyword/composite-bm25-search.ts +62 -0
- package/src/search/types.ts +35 -0
- package/src/search/vector/composite-vector-search.ts +76 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +405 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +288 -0
- package/src/services/kv-service.ts +65 -0
- package/src/services/plugin-registry.ts +109 -0
- package/src/services/watch.ts +348 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +504 -0
- package/dist/base-3SNc_CeY.d.ts +0 -593
- package/dist/chunk-424UFCY7.js.map +0 -1
- package/dist/chunk-7EZR47JV.js +0 -232
- package/dist/chunk-7EZR47JV.js.map +0 -1
- package/dist/chunk-B77KABWH.js.map +0 -1
- package/dist/chunk-CCXVL56V.js.map +0 -1
- package/dist/chunk-DI3H6JVZ.js +0 -2432
- package/dist/chunk-DI3H6JVZ.js.map +0 -1
- package/dist/chunk-FGL32LUJ.js +0 -754
- package/dist/chunk-FGL32LUJ.js.map +0 -1
- package/dist/chunk-JRSKWF6K.js +0 -313
- package/dist/chunk-JRSKWF6K.js.map +0 -1
- package/dist/chunk-U2Q2XGPZ.js +0 -42
- package/dist/chunk-U2Q2XGPZ.js.map +0 -1
- package/dist/chunk-VQ27YUHH.js +0 -629
- package/dist/chunk-VQ27YUHH.js.map +0 -1
- package/dist/chunk-VVXYZIIB.js +0 -304
- package/dist/chunk-VVXYZIIB.js.map +0 -1
- package/dist/chunk-YOLKSYWK.js +0 -79
- package/dist/chunk-YOLKSYWK.js.map +0 -1
- package/dist/chunk-ZNLN2VWV.js.map +0 -1
- package/dist/code.d.ts +0 -33
- package/dist/code.js +0 -9
- package/dist/docs.d.ts +0 -21
- package/dist/docs.js +0 -9
- package/dist/git.d.ts +0 -33
- package/dist/git.js +0 -9
- package/dist/memory.d.ts +0 -17
- package/dist/memory.js +0 -9
- package/dist/notes.d.ts +0 -17
- package/dist/notes.js +0 -10
- package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
- package/dist/resolve-CUJWY6HP.js.map +0 -1
- /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
- /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
- /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
- /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
- /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
- /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
- /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
- /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
- /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
- /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
package/dist/chunk-DI3H6JVZ.js
DELETED
|
@@ -1,2432 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
providerKey,
|
|
3
|
-
resolveEmbedding
|
|
4
|
-
} from "./chunk-B77KABWH.js";
|
|
5
|
-
import {
|
|
6
|
-
isIgnoredDir,
|
|
7
|
-
isIgnoredFile,
|
|
8
|
-
isSupported
|
|
9
|
-
} from "./chunk-FGL32LUJ.js";
|
|
10
|
-
import {
|
|
11
|
-
rerank
|
|
12
|
-
} from "./chunk-VQ27YUHH.js";
|
|
13
|
-
import {
|
|
14
|
-
normalizeBM25,
|
|
15
|
-
reciprocalRankFusion,
|
|
16
|
-
sanitizeFTS
|
|
17
|
-
} from "./chunk-YOLKSYWK.js";
|
|
18
|
-
import {
|
|
19
|
-
cosineSimilarity,
|
|
20
|
-
vecToBuffer
|
|
21
|
-
} from "./chunk-U2Q2XGPZ.js";
|
|
22
|
-
import {
|
|
23
|
-
__name
|
|
24
|
-
} from "./chunk-7QVYU63E.js";
|
|
25
|
-
|
|
26
|
-
// src/config/defaults.ts
|
|
27
|
-
import * as path from "path";
|
|
28
|
-
var DEFAULTS = {
|
|
29
|
-
repoPath: ".",
|
|
30
|
-
dbPath: ".brainbank/brainbank.db",
|
|
31
|
-
gitDepth: 500,
|
|
32
|
-
maxFileSize: 512e3,
|
|
33
|
-
// 500KB
|
|
34
|
-
maxDiffBytes: 8192,
|
|
35
|
-
hnswM: 16,
|
|
36
|
-
hnswEfConstruction: 200,
|
|
37
|
-
hnswEfSearch: 50,
|
|
38
|
-
embeddingDims: 384,
|
|
39
|
-
maxElements: 2e6
|
|
40
|
-
};
|
|
41
|
-
function resolveConfig(partial = {}) {
|
|
42
|
-
const repoPath = path.resolve(partial.repoPath ?? DEFAULTS.repoPath);
|
|
43
|
-
const rawDbPath = partial.dbPath ?? DEFAULTS.dbPath;
|
|
44
|
-
const dbPath = path.isAbsolute(rawDbPath) ? rawDbPath : path.join(repoPath, rawDbPath);
|
|
45
|
-
return {
|
|
46
|
-
repoPath,
|
|
47
|
-
dbPath,
|
|
48
|
-
gitDepth: partial.gitDepth ?? DEFAULTS.gitDepth,
|
|
49
|
-
maxFileSize: partial.maxFileSize ?? DEFAULTS.maxFileSize,
|
|
50
|
-
maxDiffBytes: partial.maxDiffBytes ?? DEFAULTS.maxDiffBytes,
|
|
51
|
-
hnswM: partial.hnswM ?? DEFAULTS.hnswM,
|
|
52
|
-
hnswEfConstruction: partial.hnswEfConstruction ?? DEFAULTS.hnswEfConstruction,
|
|
53
|
-
hnswEfSearch: partial.hnswEfSearch ?? DEFAULTS.hnswEfSearch,
|
|
54
|
-
embeddingDims: partial.embeddingDims ?? DEFAULTS.embeddingDims,
|
|
55
|
-
maxElements: partial.maxElements ?? DEFAULTS.maxElements,
|
|
56
|
-
embeddingProvider: partial.embeddingProvider,
|
|
57
|
-
reranker: partial.reranker
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
__name(resolveConfig, "resolveConfig");
|
|
61
|
-
|
|
62
|
-
// src/domain/collection.ts
|
|
63
|
-
var Collection = class {
|
|
64
|
-
constructor(_name, _db, _embedding, _hnsw, _vecs, _reranker) {
|
|
65
|
-
this._name = _name;
|
|
66
|
-
this._db = _db;
|
|
67
|
-
this._embedding = _embedding;
|
|
68
|
-
this._hnsw = _hnsw;
|
|
69
|
-
this._vecs = _vecs;
|
|
70
|
-
this._reranker = _reranker;
|
|
71
|
-
}
|
|
72
|
-
static {
|
|
73
|
-
__name(this, "Collection");
|
|
74
|
-
}
|
|
75
|
-
/** Collection name. */
|
|
76
|
-
get name() {
|
|
77
|
-
return this._name;
|
|
78
|
-
}
|
|
79
|
-
/** Add an item. Returns its ID. */
|
|
80
|
-
async add(content, options = {}) {
|
|
81
|
-
const opts = "tags" in options || "ttl" in options || "metadata" in options ? options : { metadata: options };
|
|
82
|
-
const metadata = opts.metadata ?? {};
|
|
83
|
-
const tags = opts.tags ?? [];
|
|
84
|
-
const expiresAt = opts.ttl ? Math.floor(Date.now() / 1e3) + parseDuration(opts.ttl) : null;
|
|
85
|
-
const vec = await this._embedding.embed(content);
|
|
86
|
-
const result = this._db.prepare(
|
|
87
|
-
"INSERT INTO kv_data (collection, content, meta_json, tags_json, expires_at) VALUES (?, ?, ?, ?, ?)"
|
|
88
|
-
).run(this._name, content, JSON.stringify(metadata), JSON.stringify(tags), expiresAt);
|
|
89
|
-
const id = Number(result.lastInsertRowid);
|
|
90
|
-
this._db.prepare(
|
|
91
|
-
"INSERT INTO kv_vectors (data_id, embedding) VALUES (?, ?)"
|
|
92
|
-
).run(id, vecToBuffer(vec));
|
|
93
|
-
this._hnsw.add(vec, id);
|
|
94
|
-
this._vecs.set(id, vec);
|
|
95
|
-
return id;
|
|
96
|
-
}
|
|
97
|
-
/** Add multiple items. Returns their IDs. */
|
|
98
|
-
async addMany(items) {
|
|
99
|
-
if (items.length === 0) return [];
|
|
100
|
-
const texts = items.map((i) => i.content);
|
|
101
|
-
const vecs = await this._embedding.embedBatch(texts);
|
|
102
|
-
const ids = [];
|
|
103
|
-
const insertData = this._db.prepare(
|
|
104
|
-
"INSERT INTO kv_data (collection, content, meta_json, tags_json, expires_at) VALUES (?, ?, ?, ?, ?)"
|
|
105
|
-
);
|
|
106
|
-
const insertVec = this._db.prepare(
|
|
107
|
-
"INSERT INTO kv_vectors (data_id, embedding) VALUES (?, ?)"
|
|
108
|
-
);
|
|
109
|
-
this._db.transaction(() => {
|
|
110
|
-
for (let i = 0; i < items.length; i++) {
|
|
111
|
-
const item = items[i];
|
|
112
|
-
const expiresAt = item.ttl ? Math.floor(Date.now() / 1e3) + parseDuration(item.ttl) : null;
|
|
113
|
-
const result = insertData.run(
|
|
114
|
-
this._name,
|
|
115
|
-
item.content,
|
|
116
|
-
JSON.stringify(item.metadata ?? {}),
|
|
117
|
-
JSON.stringify(item.tags ?? []),
|
|
118
|
-
expiresAt
|
|
119
|
-
);
|
|
120
|
-
const id = Number(result.lastInsertRowid);
|
|
121
|
-
insertVec.run(id, vecToBuffer(vecs[i]));
|
|
122
|
-
ids.push(id);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
for (let i = 0; i < ids.length; i++) {
|
|
126
|
-
this._hnsw.add(vecs[i], ids[i]);
|
|
127
|
-
this._vecs.set(ids[i], vecs[i]);
|
|
128
|
-
}
|
|
129
|
-
return ids;
|
|
130
|
-
}
|
|
131
|
-
/** Search this collection. */
|
|
132
|
-
async search(query, options = {}) {
|
|
133
|
-
const { k = 5, mode = "hybrid", minScore = 0.15, tags } = options;
|
|
134
|
-
this._pruneExpired();
|
|
135
|
-
if (mode === "keyword") return this._filterByTags(this._searchBM25(query, k, minScore), tags);
|
|
136
|
-
if (mode === "vector") return this._filterByTags(await this._searchVector(query, k, minScore), tags);
|
|
137
|
-
const [vectorHits, bm25Hits] = await Promise.all([
|
|
138
|
-
this._searchVector(query, k, 0),
|
|
139
|
-
Promise.resolve(this._searchBM25(query, k, 0))
|
|
140
|
-
]);
|
|
141
|
-
const fused = reciprocalRankFusion([
|
|
142
|
-
vectorHits.map((h) => ({ type: "collection", score: h.score ?? 0, content: h.content, metadata: { id: h.id } })),
|
|
143
|
-
bm25Hits.map((h) => ({ type: "collection", score: h.score ?? 0, content: h.content, metadata: { id: h.id } }))
|
|
144
|
-
]);
|
|
145
|
-
const allById = /* @__PURE__ */ new Map();
|
|
146
|
-
for (const h of [...vectorHits, ...bm25Hits]) allById.set(h.id, h);
|
|
147
|
-
const results = [];
|
|
148
|
-
for (const r of fused) {
|
|
149
|
-
const meta = r.metadata;
|
|
150
|
-
const item = allById.get(meta?.id);
|
|
151
|
-
if (!item) continue;
|
|
152
|
-
const scored = { ...item, score: r.score };
|
|
153
|
-
if (scored.score >= minScore) results.push(scored);
|
|
154
|
-
if (results.length >= k) break;
|
|
155
|
-
}
|
|
156
|
-
if (this._reranker && results.length > 1) {
|
|
157
|
-
const asSearchResults = results.map((r) => ({
|
|
158
|
-
type: "collection",
|
|
159
|
-
score: r.score ?? 0,
|
|
160
|
-
content: r.content,
|
|
161
|
-
metadata: { id: r.id }
|
|
162
|
-
}));
|
|
163
|
-
const reranked = await rerank(query, asSearchResults, this._reranker);
|
|
164
|
-
const rerankedById = new Map(reranked.map((r) => [r.metadata?.id, r.score]));
|
|
165
|
-
const blended = results.map((r) => ({ ...r, score: rerankedById.get(r.id) ?? r.score ?? 0 }));
|
|
166
|
-
return this._filterByTags(
|
|
167
|
-
blended.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
|
|
168
|
-
tags
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
return this._filterByTags(results, tags);
|
|
172
|
-
}
|
|
173
|
-
/** List items (newest first). */
|
|
174
|
-
list(options = {}) {
|
|
175
|
-
const { limit = 20, offset = 0, tags } = options;
|
|
176
|
-
this._pruneExpired();
|
|
177
|
-
const rows = this._db.prepare(
|
|
178
|
-
"SELECT * FROM kv_data WHERE collection = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?"
|
|
179
|
-
).all(this._name, Math.floor(Date.now() / 1e3), limit, offset);
|
|
180
|
-
return this._filterByTags(rows.map((r) => this._rowToItem(r)), tags);
|
|
181
|
-
}
|
|
182
|
-
/** Count items in this collection. */
|
|
183
|
-
count() {
|
|
184
|
-
return this._db.prepare(
|
|
185
|
-
"SELECT COUNT(*) as c FROM kv_data WHERE collection = ? AND (expires_at IS NULL OR expires_at > ?)"
|
|
186
|
-
).get(this._name, Math.floor(Date.now() / 1e3)).c;
|
|
187
|
-
}
|
|
188
|
-
/** Keep only the N most recent items, remove the rest. */
|
|
189
|
-
async trim(options) {
|
|
190
|
-
const before = this.count();
|
|
191
|
-
if (before <= options.keep) return { removed: 0 };
|
|
192
|
-
const toRemove = this._db.prepare(`
|
|
193
|
-
SELECT id FROM kv_data
|
|
194
|
-
WHERE collection = ?
|
|
195
|
-
ORDER BY created_at DESC, id DESC
|
|
196
|
-
LIMIT -1 OFFSET ?
|
|
197
|
-
`).all(this._name, options.keep);
|
|
198
|
-
for (const row of toRemove) {
|
|
199
|
-
this._removeById(row.id);
|
|
200
|
-
}
|
|
201
|
-
return { removed: toRemove.length };
|
|
202
|
-
}
|
|
203
|
-
/** Remove items older than a duration string (e.g. '30d', '12h'). */
|
|
204
|
-
async prune(options) {
|
|
205
|
-
const seconds = parseDuration(options.olderThan);
|
|
206
|
-
const cutoff = Math.floor(Date.now() / 1e3) - seconds;
|
|
207
|
-
const toRemove = this._db.prepare(
|
|
208
|
-
"SELECT id FROM kv_data WHERE collection = ? AND created_at < ?"
|
|
209
|
-
).all(this._name, cutoff);
|
|
210
|
-
for (const row of toRemove) {
|
|
211
|
-
this._removeById(row.id);
|
|
212
|
-
}
|
|
213
|
-
return { removed: toRemove.length };
|
|
214
|
-
}
|
|
215
|
-
/** Remove a specific item by ID. */
|
|
216
|
-
remove(id) {
|
|
217
|
-
this._removeById(id);
|
|
218
|
-
}
|
|
219
|
-
/** Clear all items in this collection. */
|
|
220
|
-
clear() {
|
|
221
|
-
const rows = this._db.prepare(
|
|
222
|
-
"SELECT id FROM kv_data WHERE collection = ?"
|
|
223
|
-
).all(this._name);
|
|
224
|
-
for (const row of rows) {
|
|
225
|
-
this._removeById(row.id);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// ── Private ──────────────────────────────────────
|
|
229
|
-
_removeById(id) {
|
|
230
|
-
this._db.prepare("DELETE FROM kv_data WHERE id = ?").run(id);
|
|
231
|
-
this._hnsw.remove(id);
|
|
232
|
-
this._vecs.delete(id);
|
|
233
|
-
}
|
|
234
|
-
async _searchVector(query, k, minScore) {
|
|
235
|
-
if (this._hnsw.size === 0) return [];
|
|
236
|
-
const queryVec = await this._embedding.embed(query);
|
|
237
|
-
const searchK = Math.min(k * 10, this._hnsw.size);
|
|
238
|
-
const hits = this._hnsw.search(queryVec, searchK);
|
|
239
|
-
const ids = hits.map((h) => h.id);
|
|
240
|
-
if (ids.length === 0) return [];
|
|
241
|
-
const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
|
|
242
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
243
|
-
const rows = this._db.prepare(
|
|
244
|
-
`SELECT * FROM kv_data WHERE id IN (${placeholders}) AND collection = ?`
|
|
245
|
-
).all(...ids, this._name);
|
|
246
|
-
return rows.map((r) => ({ ...this._rowToItem(r), score: scoreMap.get(r.id) ?? 0 })).filter((r) => r.score >= minScore).sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, k);
|
|
247
|
-
}
|
|
248
|
-
_searchBM25(query, k, minScore) {
|
|
249
|
-
const ftsQuery = sanitizeFTS(query);
|
|
250
|
-
if (!ftsQuery) return [];
|
|
251
|
-
try {
|
|
252
|
-
const rows = this._db.prepare(`
|
|
253
|
-
SELECT d.*, bm25(fts_kv, 5.0, 1.0) AS score
|
|
254
|
-
FROM fts_kv f
|
|
255
|
-
JOIN kv_data d ON d.id = f.rowid
|
|
256
|
-
WHERE fts_kv MATCH ? AND d.collection = ?
|
|
257
|
-
ORDER BY score ASC
|
|
258
|
-
LIMIT ?
|
|
259
|
-
`).all(ftsQuery, this._name, k);
|
|
260
|
-
return rows.map((r) => ({
|
|
261
|
-
...this._rowToItem(r),
|
|
262
|
-
score: normalizeBM25(r.score)
|
|
263
|
-
})).filter((r) => (r.score ?? 0) >= minScore);
|
|
264
|
-
} catch {
|
|
265
|
-
return [];
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
_rowToItem(r) {
|
|
269
|
-
return {
|
|
270
|
-
id: r.id,
|
|
271
|
-
collection: r.collection,
|
|
272
|
-
content: r.content,
|
|
273
|
-
metadata: JSON.parse(r.meta_json || "{}"),
|
|
274
|
-
tags: JSON.parse(r.tags_json || "[]"),
|
|
275
|
-
createdAt: r.created_at,
|
|
276
|
-
expiresAt: r.expires_at ?? void 0
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
/** Filter results by tags (item must have ALL specified tags). */
|
|
280
|
-
_filterByTags(items, tags) {
|
|
281
|
-
if (!tags || tags.length === 0) return items;
|
|
282
|
-
return items.filter(
|
|
283
|
-
(item) => tags.every((t) => item.tags.includes(t))
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
/** Remove expired items (TTL). Called automatically on search/list. */
|
|
287
|
-
_pruneExpired() {
|
|
288
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
289
|
-
const expired = this._db.prepare(
|
|
290
|
-
"SELECT id FROM kv_data WHERE collection = ? AND expires_at IS NOT NULL AND expires_at <= ?"
|
|
291
|
-
).all(this._name, now);
|
|
292
|
-
for (const row of expired) {
|
|
293
|
-
this._removeById(row.id);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
function parseDuration(s) {
|
|
298
|
-
const match = s.match(/^(\d+)([dhms])$/);
|
|
299
|
-
if (!match) throw new Error(`Invalid duration: "${s}". Use format like '30d', '12h', '5m'.`);
|
|
300
|
-
const n = parseInt(match[1], 10);
|
|
301
|
-
switch (match[2]) {
|
|
302
|
-
case "d":
|
|
303
|
-
return n * 86400;
|
|
304
|
-
case "h":
|
|
305
|
-
return n * 3600;
|
|
306
|
-
case "m":
|
|
307
|
-
return n * 60;
|
|
308
|
-
case "s":
|
|
309
|
-
return n;
|
|
310
|
-
default:
|
|
311
|
-
return n;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
__name(parseDuration, "parseDuration");
|
|
315
|
-
|
|
316
|
-
// src/providers/vector/hnsw-index.ts
|
|
317
|
-
import { existsSync } from "fs";
|
|
318
|
-
var HNSWIndex = class {
|
|
319
|
-
constructor(_dims, _maxElements = 2e6, _M = 16, _efConstruction = 200, _efSearch = 50) {
|
|
320
|
-
this._dims = _dims;
|
|
321
|
-
this._maxElements = _maxElements;
|
|
322
|
-
this._M = _M;
|
|
323
|
-
this._efConstruction = _efConstruction;
|
|
324
|
-
this._efSearch = _efSearch;
|
|
325
|
-
}
|
|
326
|
-
static {
|
|
327
|
-
__name(this, "HNSWIndex");
|
|
328
|
-
}
|
|
329
|
-
_index = null;
|
|
330
|
-
_lib = null;
|
|
331
|
-
_ids = /* @__PURE__ */ new Set();
|
|
332
|
-
/**
|
|
333
|
-
* Initialize the HNSW index.
|
|
334
|
-
* Must be called before add/search.
|
|
335
|
-
*/
|
|
336
|
-
async init() {
|
|
337
|
-
this._lib = await import("hnswlib-node");
|
|
338
|
-
this._createIndex();
|
|
339
|
-
return this;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Reinitialize the index in-place, clearing all vectors.
|
|
343
|
-
* Required after reembed or full re-index to avoid duplicate IDs.
|
|
344
|
-
* init() must have been called first.
|
|
345
|
-
*/
|
|
346
|
-
reinit() {
|
|
347
|
-
if (!this._lib) throw new Error("HNSW not initialized \u2014 call init() first");
|
|
348
|
-
this._createIndex();
|
|
349
|
-
}
|
|
350
|
-
_createIndex() {
|
|
351
|
-
const HNSW = this._lib.default?.HierarchicalNSW ?? this._lib.HierarchicalNSW;
|
|
352
|
-
this._index = new HNSW("cosine", this._dims);
|
|
353
|
-
this._index.initIndex(this._maxElements, this._M, this._efConstruction);
|
|
354
|
-
this._index.setEf(this._efSearch);
|
|
355
|
-
this._ids = /* @__PURE__ */ new Set();
|
|
356
|
-
}
|
|
357
|
-
/** Maximum capacity of this index. */
|
|
358
|
-
get maxElements() {
|
|
359
|
-
return this._maxElements;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Add a vector with an integer ID.
|
|
363
|
-
* The vector should be pre-normalized for cosine distance.
|
|
364
|
-
*/
|
|
365
|
-
add(vector, id) {
|
|
366
|
-
if (!this._index) throw new Error("HNSW index not initialized \u2014 call init() first");
|
|
367
|
-
if (this._ids.has(id)) return;
|
|
368
|
-
if (this._ids.size >= this._maxElements) {
|
|
369
|
-
throw new Error(
|
|
370
|
-
`HNSW index full (${this._maxElements} elements). Increase maxElements in config or prune old data.`
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
this._index.addPoint(Array.from(vector), id);
|
|
374
|
-
this._ids.add(id);
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Mark a vector as deleted so it no longer appears in searches.
|
|
378
|
-
* Uses hnswlib-node markDelete under the hood.
|
|
379
|
-
* Safe to call with an ID that doesn't exist.
|
|
380
|
-
*/
|
|
381
|
-
remove(id) {
|
|
382
|
-
if (!this._index || this._ids.size === 0) return;
|
|
383
|
-
if (!this._ids.has(id)) return;
|
|
384
|
-
try {
|
|
385
|
-
this._index.markDelete(id);
|
|
386
|
-
this._ids.delete(id);
|
|
387
|
-
} catch {
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Search for the k nearest neighbors.
|
|
392
|
-
* Returns results sorted by score (highest first).
|
|
393
|
-
* Score is 1 - cosine_distance (1.0 = identical).
|
|
394
|
-
*/
|
|
395
|
-
search(query, k) {
|
|
396
|
-
if (!this._index || this._ids.size === 0) return [];
|
|
397
|
-
const actualK = Math.min(k, this._ids.size);
|
|
398
|
-
const result = this._index.searchKnn(Array.from(query), actualK);
|
|
399
|
-
return result.neighbors.map((id, i) => ({
|
|
400
|
-
id,
|
|
401
|
-
score: 1 - result.distances[i]
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
/** Number of vectors in the index. */
|
|
405
|
-
get size() {
|
|
406
|
-
return this._ids.size;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Save the HNSW graph to disk.
|
|
410
|
-
* The file can be loaded later with tryLoad() to skip vector-by-vector insertion.
|
|
411
|
-
*/
|
|
412
|
-
save(path4) {
|
|
413
|
-
if (!this._index || this._ids.size === 0) return;
|
|
414
|
-
this._index.writeIndexSync(path4);
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Try to load a previously saved HNSW index from disk.
|
|
418
|
-
* Returns true if loaded successfully, false if stale or missing.
|
|
419
|
-
* @param path File path to the saved index
|
|
420
|
-
* @param expectedCount Expected number of vectors (from SQLite) — used to detect staleness
|
|
421
|
-
*/
|
|
422
|
-
tryLoad(path4, expectedCount) {
|
|
423
|
-
if (!this._index || !existsSync(path4)) return false;
|
|
424
|
-
try {
|
|
425
|
-
this._index.readIndexSync(path4);
|
|
426
|
-
const loadedCount = this._index.getCurrentCount();
|
|
427
|
-
if (loadedCount !== expectedCount) {
|
|
428
|
-
this.reinit();
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
|
-
const ids = this._index.getIdsList();
|
|
432
|
-
this._ids = new Set(ids);
|
|
433
|
-
this._index.setEf(this._efSearch);
|
|
434
|
-
return true;
|
|
435
|
-
} catch {
|
|
436
|
-
this.reinit();
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
// src/search/vector/mmr.ts
|
|
443
|
-
function searchMMR(index, query, vectorCache, k, lambda = 0.7) {
|
|
444
|
-
const candidates = index.search(query, k * 3);
|
|
445
|
-
if (candidates.length <= k) return candidates;
|
|
446
|
-
const selected = [];
|
|
447
|
-
const remaining = [...candidates];
|
|
448
|
-
while (selected.length < k && remaining.length > 0) {
|
|
449
|
-
let bestScore = -Infinity;
|
|
450
|
-
let bestIdx = 0;
|
|
451
|
-
for (let i = 0; i < remaining.length; i++) {
|
|
452
|
-
const relevance = remaining[i].score;
|
|
453
|
-
let maxSim = 0;
|
|
454
|
-
for (const sel of selected) {
|
|
455
|
-
const candidateVec = vectorCache.get(remaining[i].id);
|
|
456
|
-
const selectedVec = vectorCache.get(sel.id);
|
|
457
|
-
if (candidateVec && selectedVec) {
|
|
458
|
-
maxSim = Math.max(maxSim, cosineSimilarity(candidateVec, selectedVec));
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
const mmrScore = lambda * relevance - (1 - lambda) * maxSim;
|
|
462
|
-
if (mmrScore > bestScore) {
|
|
463
|
-
bestScore = mmrScore;
|
|
464
|
-
bestIdx = i;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
selected.push(remaining[bestIdx]);
|
|
468
|
-
remaining.splice(bestIdx, 1);
|
|
469
|
-
}
|
|
470
|
-
return selected;
|
|
471
|
-
}
|
|
472
|
-
__name(searchMMR, "searchMMR");
|
|
473
|
-
|
|
474
|
-
// src/search/vector/vector-search.ts
|
|
475
|
-
var VectorSearch = class {
|
|
476
|
-
static {
|
|
477
|
-
__name(this, "VectorSearch");
|
|
478
|
-
}
|
|
479
|
-
_config;
|
|
480
|
-
constructor(config) {
|
|
481
|
-
this._config = config;
|
|
482
|
-
}
|
|
483
|
-
/** Search across all indices. Returns combined results sorted by score. */
|
|
484
|
-
async search(query, options = {}) {
|
|
485
|
-
const {
|
|
486
|
-
codeK = 6,
|
|
487
|
-
gitK = 5,
|
|
488
|
-
patternK = 4,
|
|
489
|
-
minScore = 0.25,
|
|
490
|
-
useMMR = true,
|
|
491
|
-
mmrLambda = 0.7
|
|
492
|
-
} = options;
|
|
493
|
-
const queryVec = await this._config.embedding.embed(query);
|
|
494
|
-
const results = [];
|
|
495
|
-
this._searchCode(queryVec, codeK, minScore, useMMR, mmrLambda, results);
|
|
496
|
-
this._searchGit(queryVec, gitK, minScore, results);
|
|
497
|
-
this._searchPatterns(queryVec, patternK, minScore, useMMR, mmrLambda, results);
|
|
498
|
-
results.sort((a, b) => b.score - a.score);
|
|
499
|
-
if (this._config.reranker && results.length > 1) {
|
|
500
|
-
return rerank(query, results, this._config.reranker);
|
|
501
|
-
}
|
|
502
|
-
return results;
|
|
503
|
-
}
|
|
504
|
-
/** Vector search across code chunks. */
|
|
505
|
-
_searchCode(queryVec, k, minScore, useMMR, mmrLambda, results) {
|
|
506
|
-
const { codeHnsw, codeVecs, db } = this._config;
|
|
507
|
-
if (!codeHnsw || codeHnsw.size === 0) return;
|
|
508
|
-
const hits = useMMR ? searchMMR(codeHnsw, queryVec, codeVecs, k, mmrLambda) : codeHnsw.search(queryVec, k);
|
|
509
|
-
if (hits.length === 0) return;
|
|
510
|
-
const ids = hits.map((h) => h.id);
|
|
511
|
-
const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
|
|
512
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
513
|
-
const rows = db.prepare(
|
|
514
|
-
`SELECT * FROM code_chunks WHERE id IN (${placeholders})`
|
|
515
|
-
).all(...ids);
|
|
516
|
-
for (const r of rows) {
|
|
517
|
-
const score = scoreMap.get(r.id) ?? 0;
|
|
518
|
-
if (score >= minScore) {
|
|
519
|
-
results.push({
|
|
520
|
-
type: "code",
|
|
521
|
-
score,
|
|
522
|
-
filePath: r.file_path,
|
|
523
|
-
content: r.content,
|
|
524
|
-
metadata: {
|
|
525
|
-
chunkType: r.chunk_type,
|
|
526
|
-
name: r.name,
|
|
527
|
-
startLine: r.start_line,
|
|
528
|
-
endLine: r.end_line,
|
|
529
|
-
language: r.language
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
/** Vector search across git commits. */
|
|
536
|
-
_searchGit(queryVec, k, minScore, results) {
|
|
537
|
-
const { gitHnsw, db } = this._config;
|
|
538
|
-
if (!gitHnsw || gitHnsw.size === 0) return;
|
|
539
|
-
const hits = gitHnsw.search(queryVec, k * 2);
|
|
540
|
-
if (hits.length === 0) return;
|
|
541
|
-
const ids = hits.map((h) => h.id);
|
|
542
|
-
const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
|
|
543
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
544
|
-
const rows = db.prepare(
|
|
545
|
-
`SELECT * FROM git_commits WHERE id IN (${placeholders}) AND is_merge = 0`
|
|
546
|
-
).all(...ids);
|
|
547
|
-
for (const r of rows) {
|
|
548
|
-
const score = scoreMap.get(r.id) ?? 0;
|
|
549
|
-
if (score >= minScore) {
|
|
550
|
-
results.push({
|
|
551
|
-
type: "commit",
|
|
552
|
-
score,
|
|
553
|
-
content: r.message,
|
|
554
|
-
metadata: {
|
|
555
|
-
hash: r.hash,
|
|
556
|
-
shortHash: r.short_hash,
|
|
557
|
-
author: r.author,
|
|
558
|
-
date: r.date,
|
|
559
|
-
files: JSON.parse(r.files_json ?? "[]"),
|
|
560
|
-
additions: r.additions,
|
|
561
|
-
deletions: r.deletions,
|
|
562
|
-
diff: r.diff
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
/** Vector search across memory patterns. */
|
|
569
|
-
_searchPatterns(queryVec, k, minScore, useMMR, mmrLambda, results) {
|
|
570
|
-
const { patternHnsw, patternVecs, db } = this._config;
|
|
571
|
-
if (!patternHnsw || patternHnsw.size === 0) return;
|
|
572
|
-
const hits = useMMR ? searchMMR(patternHnsw, queryVec, patternVecs, k, mmrLambda) : patternHnsw.search(queryVec, k);
|
|
573
|
-
if (hits.length === 0) return;
|
|
574
|
-
const ids = hits.map((h) => h.id);
|
|
575
|
-
const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
|
|
576
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
577
|
-
const rows = db.prepare(
|
|
578
|
-
`SELECT * FROM memory_patterns WHERE id IN (${placeholders}) AND success_rate >= 0.5`
|
|
579
|
-
).all(...ids);
|
|
580
|
-
for (const r of rows) {
|
|
581
|
-
const score = scoreMap.get(r.id) ?? 0;
|
|
582
|
-
if (score >= minScore) {
|
|
583
|
-
results.push({
|
|
584
|
-
type: "pattern",
|
|
585
|
-
score,
|
|
586
|
-
content: r.approach,
|
|
587
|
-
metadata: {
|
|
588
|
-
taskType: r.task_type,
|
|
589
|
-
task: r.task,
|
|
590
|
-
outcome: r.outcome,
|
|
591
|
-
successRate: r.success_rate,
|
|
592
|
-
critique: r.critique
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
// src/search/keyword/keyword-search.ts
|
|
601
|
-
function isFTSError(e) {
|
|
602
|
-
return e instanceof Error && /fts5|syntax error|parse error/i.test(e.message);
|
|
603
|
-
}
|
|
604
|
-
__name(isFTSError, "isFTSError");
|
|
605
|
-
var KeywordSearch = class {
|
|
606
|
-
constructor(_db) {
|
|
607
|
-
this._db = _db;
|
|
608
|
-
}
|
|
609
|
-
static {
|
|
610
|
-
__name(this, "KeywordSearch");
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Full-text keyword search across all FTS5 indices.
|
|
614
|
-
* Uses BM25 scoring — lower scores = better matches.
|
|
615
|
-
*/
|
|
616
|
-
async search(query, options = {}) {
|
|
617
|
-
const { codeK = 8, gitK = 5, patternK = 4 } = options;
|
|
618
|
-
const ftsQuery = sanitizeFTS(query);
|
|
619
|
-
if (!ftsQuery) return [];
|
|
620
|
-
const results = [];
|
|
621
|
-
if (codeK > 0) this._searchCode(ftsQuery, query, codeK, results);
|
|
622
|
-
if (gitK > 0) this._searchGit(ftsQuery, gitK, results);
|
|
623
|
-
if (patternK > 0) this._searchPatterns(ftsQuery, patternK, results);
|
|
624
|
-
return results.sort((a, b) => b.score - a.score);
|
|
625
|
-
}
|
|
626
|
-
/** FTS5 search across code chunks + file-path fallback. */
|
|
627
|
-
_searchCode(ftsQuery, rawQuery, k, results) {
|
|
628
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
629
|
-
try {
|
|
630
|
-
const rows = this._db.prepare(`
|
|
631
|
-
SELECT c.id, c.file_path, c.chunk_type, c.name, c.start_line, c.end_line,
|
|
632
|
-
c.content, c.language, bm25(fts_code, 5.0, 3.0, 1.0) AS score
|
|
633
|
-
FROM fts_code f
|
|
634
|
-
JOIN code_chunks c ON c.id = f.rowid
|
|
635
|
-
WHERE fts_code MATCH ?
|
|
636
|
-
ORDER BY score ASC
|
|
637
|
-
LIMIT ?
|
|
638
|
-
`).all(ftsQuery, k);
|
|
639
|
-
for (const r of rows) {
|
|
640
|
-
seenIds.add(r.id);
|
|
641
|
-
results.push(this._toCodeResult(r, normalizeBM25(r.score), "bm25"));
|
|
642
|
-
}
|
|
643
|
-
} catch (e) {
|
|
644
|
-
if (!isFTSError(e)) throw e;
|
|
645
|
-
}
|
|
646
|
-
this._searchCodeByPath(rawQuery, seenIds, results);
|
|
647
|
-
}
|
|
648
|
-
/** File-path fallback: match filenames via LIKE. */
|
|
649
|
-
_searchCodeByPath(rawQuery, seenIds, results) {
|
|
650
|
-
try {
|
|
651
|
-
const words = rawQuery.replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
652
|
-
for (const word of words.slice(0, 3)) {
|
|
653
|
-
const pathRows = this._db.prepare(`
|
|
654
|
-
SELECT id, file_path, chunk_type, name, start_line, end_line, content, language
|
|
655
|
-
FROM code_chunks
|
|
656
|
-
WHERE file_path LIKE ? AND chunk_type = 'file'
|
|
657
|
-
LIMIT 3
|
|
658
|
-
`).all(`%${word}%`);
|
|
659
|
-
for (const r of pathRows) {
|
|
660
|
-
if (seenIds.has(r.id)) continue;
|
|
661
|
-
seenIds.add(r.id);
|
|
662
|
-
results.push(this._toCodeResult(r, 0.6, "bm25-path"));
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
} catch (e) {
|
|
666
|
-
if (!isFTSError(e)) throw e;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
/** FTS5 search across git commits. */
|
|
670
|
-
_searchGit(ftsQuery, k, results) {
|
|
671
|
-
try {
|
|
672
|
-
const rows = this._db.prepare(`
|
|
673
|
-
SELECT c.id, c.hash, c.short_hash, c.message, c.author, c.date,
|
|
674
|
-
c.files_json, c.diff, c.additions, c.deletions,
|
|
675
|
-
bm25(fts_commits, 5.0, 2.0, 1.0) AS score
|
|
676
|
-
FROM fts_commits f
|
|
677
|
-
JOIN git_commits c ON c.id = f.rowid
|
|
678
|
-
WHERE fts_commits MATCH ? AND c.is_merge = 0
|
|
679
|
-
ORDER BY score ASC
|
|
680
|
-
LIMIT ?
|
|
681
|
-
`).all(ftsQuery, k);
|
|
682
|
-
for (const r of rows) {
|
|
683
|
-
results.push({
|
|
684
|
-
type: "commit",
|
|
685
|
-
score: normalizeBM25(r.score),
|
|
686
|
-
content: r.message,
|
|
687
|
-
metadata: {
|
|
688
|
-
hash: r.hash,
|
|
689
|
-
shortHash: r.short_hash,
|
|
690
|
-
author: r.author,
|
|
691
|
-
date: r.date,
|
|
692
|
-
files: JSON.parse(r.files_json ?? "[]"),
|
|
693
|
-
additions: r.additions,
|
|
694
|
-
deletions: r.deletions,
|
|
695
|
-
diff: r.diff,
|
|
696
|
-
searchType: "bm25"
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
} catch (e) {
|
|
701
|
-
if (!isFTSError(e)) throw e;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
/** FTS5 search across memory patterns. */
|
|
705
|
-
_searchPatterns(ftsQuery, k, results) {
|
|
706
|
-
try {
|
|
707
|
-
const rows = this._db.prepare(`
|
|
708
|
-
SELECT p.id, p.task_type, p.task, p.approach, p.outcome,
|
|
709
|
-
p.success_rate, p.critique,
|
|
710
|
-
bm25(fts_patterns, 3.0, 5.0, 5.0, 1.0) AS score
|
|
711
|
-
FROM fts_patterns f
|
|
712
|
-
JOIN memory_patterns p ON p.id = f.rowid
|
|
713
|
-
WHERE fts_patterns MATCH ? AND p.success_rate >= 0.5
|
|
714
|
-
ORDER BY score ASC
|
|
715
|
-
LIMIT ?
|
|
716
|
-
`).all(ftsQuery, k);
|
|
717
|
-
for (const r of rows) {
|
|
718
|
-
results.push({
|
|
719
|
-
type: "pattern",
|
|
720
|
-
score: normalizeBM25(r.score),
|
|
721
|
-
content: r.approach,
|
|
722
|
-
metadata: {
|
|
723
|
-
taskType: r.task_type,
|
|
724
|
-
task: r.task,
|
|
725
|
-
outcome: r.outcome,
|
|
726
|
-
successRate: r.success_rate,
|
|
727
|
-
critique: r.critique,
|
|
728
|
-
searchType: "bm25"
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
} catch (e) {
|
|
733
|
-
if (!isFTSError(e)) throw e;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
/** Map a code_chunks row to a CodeResult. */
|
|
737
|
-
_toCodeResult(r, score, searchType) {
|
|
738
|
-
return {
|
|
739
|
-
type: "code",
|
|
740
|
-
score,
|
|
741
|
-
filePath: r.file_path,
|
|
742
|
-
content: r.content,
|
|
743
|
-
metadata: {
|
|
744
|
-
chunkType: r.chunk_type,
|
|
745
|
-
name: r.name,
|
|
746
|
-
startLine: r.start_line,
|
|
747
|
-
endLine: r.end_line,
|
|
748
|
-
language: r.language,
|
|
749
|
-
searchType
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
/** Rebuild the FTS index from scratch. */
|
|
754
|
-
rebuild() {
|
|
755
|
-
try {
|
|
756
|
-
this._db.prepare("INSERT INTO fts_code(fts_code) VALUES('rebuild')").run();
|
|
757
|
-
this._db.prepare("INSERT INTO fts_commits(fts_commits) VALUES('rebuild')").run();
|
|
758
|
-
this._db.prepare("INSERT INTO fts_patterns(fts_patterns) VALUES('rebuild')").run();
|
|
759
|
-
} catch {
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// src/search/context-builder.ts
|
|
765
|
-
var ContextBuilder = class {
|
|
766
|
-
constructor(_search, _coEdits) {
|
|
767
|
-
this._search = _search;
|
|
768
|
-
this._coEdits = _coEdits;
|
|
769
|
-
}
|
|
770
|
-
static {
|
|
771
|
-
__name(this, "ContextBuilder");
|
|
772
|
-
}
|
|
773
|
-
/** Build a full context block for a task. Returns markdown for system prompt. */
|
|
774
|
-
async build(task, options = {}) {
|
|
775
|
-
const {
|
|
776
|
-
codeResults = 6,
|
|
777
|
-
gitResults = 5,
|
|
778
|
-
patternResults = 4,
|
|
779
|
-
affectedFiles = [],
|
|
780
|
-
minScore = 0.25,
|
|
781
|
-
useMMR = true,
|
|
782
|
-
mmrLambda = 0.7
|
|
783
|
-
} = options;
|
|
784
|
-
const results = await this._search.search(task, {
|
|
785
|
-
codeK: codeResults,
|
|
786
|
-
gitK: gitResults,
|
|
787
|
-
patternK: patternResults,
|
|
788
|
-
minScore,
|
|
789
|
-
useMMR,
|
|
790
|
-
mmrLambda
|
|
791
|
-
});
|
|
792
|
-
const parts = [`# Context for: "${task}"
|
|
793
|
-
`];
|
|
794
|
-
this._formatCodeResults(results, codeResults, parts);
|
|
795
|
-
this._formatGitResults(results, gitResults, parts);
|
|
796
|
-
this._formatCoEdits(affectedFiles, parts);
|
|
797
|
-
this._formatPatternResults(results, patternResults, parts);
|
|
798
|
-
return parts.join("\n");
|
|
799
|
-
}
|
|
800
|
-
/** Format code search results grouped by file. */
|
|
801
|
-
_formatCodeResults(results, limit, parts) {
|
|
802
|
-
const codeHits = results.filter((r) => r.type === "code").slice(0, limit);
|
|
803
|
-
if (codeHits.length === 0) return;
|
|
804
|
-
parts.push("## Relevant Code\n");
|
|
805
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
806
|
-
for (const r of codeHits) {
|
|
807
|
-
const key = r.filePath ?? "unknown";
|
|
808
|
-
if (!byFile.has(key)) byFile.set(key, []);
|
|
809
|
-
byFile.get(key).push(r);
|
|
810
|
-
}
|
|
811
|
-
for (const [file, chunks] of byFile) {
|
|
812
|
-
parts.push(`### ${file}`);
|
|
813
|
-
for (const c of chunks) {
|
|
814
|
-
const m = c.metadata;
|
|
815
|
-
const label = m.name ? `${m.chunkType} \`${m.name}\` (L${m.startLine}-${m.endLine})` : `L${m.startLine}-${m.endLine}`;
|
|
816
|
-
parts.push(`**${label}** \u2014 ${Math.round(c.score * 100)}% match`);
|
|
817
|
-
parts.push("```" + (m.language || ""));
|
|
818
|
-
parts.push(c.content);
|
|
819
|
-
parts.push("```\n");
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
/** Format git commit results with diff snippets. */
|
|
824
|
-
_formatGitResults(results, limit, parts) {
|
|
825
|
-
const gitHits = results.filter((r) => r.type === "commit").slice(0, limit);
|
|
826
|
-
if (gitHits.length === 0) return;
|
|
827
|
-
parts.push("## Related Git History\n");
|
|
828
|
-
for (const c of gitHits) {
|
|
829
|
-
const m = c.metadata;
|
|
830
|
-
const score = Math.round(c.score * 100);
|
|
831
|
-
const files = (m.files ?? []).slice(0, 4).join(", ");
|
|
832
|
-
parts.push(`**[${m.shortHash}]** ${c.content} *(${m.author}, ${m.date?.slice(0, 10)}, ${score}%)*`);
|
|
833
|
-
if (files) parts.push(` Files: ${files}`);
|
|
834
|
-
if (m.diff) {
|
|
835
|
-
const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
|
|
836
|
-
if (snippet) {
|
|
837
|
-
parts.push("```diff");
|
|
838
|
-
parts.push(snippet);
|
|
839
|
-
parts.push("```");
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
parts.push("");
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
/** Format co-edit suggestions for affected files. */
|
|
846
|
-
_formatCoEdits(affectedFiles, parts) {
|
|
847
|
-
if (affectedFiles.length === 0 || !this._coEdits) return;
|
|
848
|
-
const coEditLines = [];
|
|
849
|
-
for (const file of affectedFiles.slice(0, 3)) {
|
|
850
|
-
const suggestions = this._coEdits.suggest(file, 4);
|
|
851
|
-
if (suggestions.length > 0) {
|
|
852
|
-
coEditLines.push(
|
|
853
|
-
`- **${file}** \u2192 also tends to change: ${suggestions.map((s) => `${s.file} (${s.count}x)`).join(", ")}`
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (coEditLines.length > 0) {
|
|
858
|
-
parts.push("## Co-Edit Patterns\n");
|
|
859
|
-
parts.push(...coEditLines);
|
|
860
|
-
parts.push("");
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
/** Format memory pattern results. */
|
|
864
|
-
_formatPatternResults(results, limit, parts) {
|
|
865
|
-
const memHits = results.filter((r) => r.type === "pattern").slice(0, limit);
|
|
866
|
-
if (memHits.length === 0) return;
|
|
867
|
-
parts.push("## Learned Patterns\n");
|
|
868
|
-
for (const p of memHits) {
|
|
869
|
-
const m = p.metadata;
|
|
870
|
-
const score = Math.round(p.score * 100);
|
|
871
|
-
const success = Math.round((m.successRate ?? 0) * 100);
|
|
872
|
-
parts.push(`**${m.taskType}** \u2014 ${success}% success, ${score}% match`);
|
|
873
|
-
parts.push(`Task: ${m.task}`);
|
|
874
|
-
parts.push(`Approach: ${p.content}`);
|
|
875
|
-
if (m.critique) parts.push(`Lesson: ${m.critique}`);
|
|
876
|
-
parts.push("");
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
// src/brainbank.ts
|
|
882
|
-
import { EventEmitter } from "events";
|
|
883
|
-
|
|
884
|
-
// src/bootstrap/registry.ts
|
|
885
|
-
var ALIASES = {};
|
|
886
|
-
var PluginRegistry = class {
|
|
887
|
-
static {
|
|
888
|
-
__name(this, "PluginRegistry");
|
|
889
|
-
}
|
|
890
|
-
_map = /* @__PURE__ */ new Map();
|
|
891
|
-
// ── Registration ────────────────────────────────
|
|
892
|
-
/** Store a plugin. Duplicate names silently overwrite. */
|
|
893
|
-
register(plugin) {
|
|
894
|
-
this._map.set(plugin.name, plugin);
|
|
895
|
-
}
|
|
896
|
-
// ── Lookup ──────────────────────────────────────
|
|
897
|
-
/**
|
|
898
|
-
* Check whether a plugin is registered.
|
|
899
|
-
* Supports type-prefix matching: `has('code')` returns true if
|
|
900
|
-
* 'code', 'code:frontend', or 'code:backend' is registered.
|
|
901
|
-
*/
|
|
902
|
-
has(name) {
|
|
903
|
-
if (this._map.has(name)) return true;
|
|
904
|
-
for (const key of this._map.keys()) {
|
|
905
|
-
if (key.startsWith(name + ":")) return true;
|
|
906
|
-
}
|
|
907
|
-
return false;
|
|
908
|
-
}
|
|
909
|
-
/**
|
|
910
|
-
* Get a plugin by name. Throws a descriptive error if not found.
|
|
911
|
-
*
|
|
912
|
-
* Resolution order:
|
|
913
|
-
* 1. Alias map (currently empty)
|
|
914
|
-
* 2. Exact match
|
|
915
|
-
* 3. First type-prefix match ('code' → 'code:frontend')
|
|
916
|
-
*/
|
|
917
|
-
get(name) {
|
|
918
|
-
const resolved = ALIASES[name] ?? name;
|
|
919
|
-
const exact = this._map.get(resolved);
|
|
920
|
-
if (exact) return exact;
|
|
921
|
-
const prefixed = this.firstByType(name);
|
|
922
|
-
if (prefixed) return prefixed;
|
|
923
|
-
throw new Error(
|
|
924
|
-
`BrainBank: Plugin '${name}' is not loaded. Add .use(${name}()) to your BrainBank instance.`
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Return every plugin whose name equals `type` or starts with `type + ':'`.
|
|
929
|
-
* Example: allByType('code') → [code, code:frontend, code:backend]
|
|
930
|
-
*/
|
|
931
|
-
allByType(type) {
|
|
932
|
-
return [...this._map.values()].filter(
|
|
933
|
-
(m) => m.name === type || m.name.startsWith(type + ":")
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
/** Return the first plugin that matches the type prefix, or undefined. */
|
|
937
|
-
firstByType(type) {
|
|
938
|
-
for (const m of this._map.values()) {
|
|
939
|
-
if (m.name === type || m.name.startsWith(type + ":")) return m;
|
|
940
|
-
}
|
|
941
|
-
return void 0;
|
|
942
|
-
}
|
|
943
|
-
// ── Accessors ───────────────────────────────────
|
|
944
|
-
/** All registered plugin names (insertion order). */
|
|
945
|
-
get names() {
|
|
946
|
-
return [...this._map.keys()];
|
|
947
|
-
}
|
|
948
|
-
/** All registered plugin instances (insertion order). */
|
|
949
|
-
get all() {
|
|
950
|
-
return [...this._map.values()];
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* Underlying Map.
|
|
954
|
-
* Prefer `all`, `allByType`, or `firstByType` everywhere else.
|
|
955
|
-
*/
|
|
956
|
-
get raw() {
|
|
957
|
-
return this._map;
|
|
958
|
-
}
|
|
959
|
-
// ── Lifecycle ───────────────────────────────────
|
|
960
|
-
/** Remove all registered plugins. Called by BrainBank.close(). */
|
|
961
|
-
clear() {
|
|
962
|
-
this._map.clear();
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
// src/bootstrap/initializer.ts
|
|
967
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
968
|
-
|
|
969
|
-
// src/db/database.ts
|
|
970
|
-
import BetterSqlite3 from "better-sqlite3";
|
|
971
|
-
import * as fs from "fs";
|
|
972
|
-
import * as path2 from "path";
|
|
973
|
-
|
|
974
|
-
// src/db/schema.ts
|
|
975
|
-
var SCHEMA_VERSION = 4;
|
|
976
|
-
function createSchema(db) {
|
|
977
|
-
db.exec(`
|
|
978
|
-
-- \u2500\u2500 Schema versioning \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
979
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
980
|
-
version INTEGER PRIMARY KEY,
|
|
981
|
-
applied_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
982
|
-
);
|
|
983
|
-
INSERT OR IGNORE INTO schema_version (version) VALUES (${SCHEMA_VERSION});
|
|
984
|
-
|
|
985
|
-
-- \u2500\u2500 Code chunks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
986
|
-
CREATE TABLE IF NOT EXISTS code_chunks (
|
|
987
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
988
|
-
file_path TEXT NOT NULL,
|
|
989
|
-
chunk_type TEXT NOT NULL,
|
|
990
|
-
name TEXT,
|
|
991
|
-
start_line INTEGER NOT NULL,
|
|
992
|
-
end_line INTEGER NOT NULL,
|
|
993
|
-
content TEXT NOT NULL,
|
|
994
|
-
language TEXT NOT NULL,
|
|
995
|
-
file_hash TEXT,
|
|
996
|
-
indexed_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
997
|
-
);
|
|
998
|
-
|
|
999
|
-
CREATE TABLE IF NOT EXISTS code_vectors (
|
|
1000
|
-
chunk_id INTEGER PRIMARY KEY REFERENCES code_chunks(id) ON DELETE CASCADE,
|
|
1001
|
-
embedding BLOB NOT NULL
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
CREATE TABLE IF NOT EXISTS indexed_files (
|
|
1005
|
-
file_path TEXT PRIMARY KEY,
|
|
1006
|
-
file_hash TEXT NOT NULL,
|
|
1007
|
-
indexed_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1008
|
-
);
|
|
1009
|
-
|
|
1010
|
-
-- \u2500\u2500 Git history \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1011
|
-
CREATE TABLE IF NOT EXISTS git_commits (
|
|
1012
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1013
|
-
hash TEXT UNIQUE NOT NULL,
|
|
1014
|
-
short_hash TEXT NOT NULL,
|
|
1015
|
-
message TEXT NOT NULL,
|
|
1016
|
-
author TEXT NOT NULL,
|
|
1017
|
-
date TEXT NOT NULL,
|
|
1018
|
-
timestamp INTEGER NOT NULL,
|
|
1019
|
-
files_json TEXT NOT NULL,
|
|
1020
|
-
diff TEXT,
|
|
1021
|
-
additions INTEGER DEFAULT 0,
|
|
1022
|
-
deletions INTEGER DEFAULT 0,
|
|
1023
|
-
is_merge INTEGER DEFAULT 0
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
CREATE TABLE IF NOT EXISTS commit_files (
|
|
1027
|
-
commit_id INTEGER NOT NULL REFERENCES git_commits(id),
|
|
1028
|
-
file_path TEXT NOT NULL
|
|
1029
|
-
);
|
|
1030
|
-
|
|
1031
|
-
CREATE TABLE IF NOT EXISTS co_edits (
|
|
1032
|
-
file_a TEXT NOT NULL,
|
|
1033
|
-
file_b TEXT NOT NULL,
|
|
1034
|
-
count INTEGER NOT NULL DEFAULT 1,
|
|
1035
|
-
PRIMARY KEY (file_a, file_b)
|
|
1036
|
-
);
|
|
1037
|
-
|
|
1038
|
-
CREATE TABLE IF NOT EXISTS git_vectors (
|
|
1039
|
-
commit_id INTEGER PRIMARY KEY REFERENCES git_commits(id) ON DELETE CASCADE,
|
|
1040
|
-
embedding BLOB NOT NULL
|
|
1041
|
-
);
|
|
1042
|
-
|
|
1043
|
-
-- \u2500\u2500 Agent memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1044
|
-
CREATE TABLE IF NOT EXISTS memory_patterns (
|
|
1045
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1046
|
-
task_type TEXT NOT NULL,
|
|
1047
|
-
task TEXT NOT NULL,
|
|
1048
|
-
approach TEXT NOT NULL,
|
|
1049
|
-
outcome TEXT,
|
|
1050
|
-
success_rate REAL NOT NULL DEFAULT 0.5,
|
|
1051
|
-
critique TEXT,
|
|
1052
|
-
tokens_used INTEGER,
|
|
1053
|
-
latency_ms INTEGER,
|
|
1054
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1055
|
-
);
|
|
1056
|
-
|
|
1057
|
-
CREATE TABLE IF NOT EXISTS memory_vectors (
|
|
1058
|
-
pattern_id INTEGER PRIMARY KEY REFERENCES memory_patterns(id) ON DELETE CASCADE,
|
|
1059
|
-
embedding BLOB NOT NULL
|
|
1060
|
-
);
|
|
1061
|
-
|
|
1062
|
-
CREATE TABLE IF NOT EXISTS distilled_strategies (
|
|
1063
|
-
task_type TEXT PRIMARY KEY,
|
|
1064
|
-
strategy TEXT NOT NULL,
|
|
1065
|
-
confidence REAL NOT NULL DEFAULT 0.8,
|
|
1066
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1067
|
-
);
|
|
1068
|
-
|
|
1069
|
-
-- \u2500\u2500 Indices \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1070
|
-
CREATE INDEX IF NOT EXISTS idx_cc_file ON code_chunks(file_path);
|
|
1071
|
-
CREATE INDEX IF NOT EXISTS idx_cf_path ON commit_files(file_path);
|
|
1072
|
-
CREATE INDEX IF NOT EXISTS idx_gc_ts ON git_commits(timestamp DESC);
|
|
1073
|
-
CREATE INDEX IF NOT EXISTS idx_gc_hash ON git_commits(hash);
|
|
1074
|
-
CREATE INDEX IF NOT EXISTS idx_mp_type ON memory_patterns(task_type);
|
|
1075
|
-
CREATE INDEX IF NOT EXISTS idx_mp_success ON memory_patterns(success_rate);
|
|
1076
|
-
CREATE INDEX IF NOT EXISTS idx_mp_created ON memory_patterns(created_at);
|
|
1077
|
-
|
|
1078
|
-
-- \u2500\u2500 FTS5 Full-Text Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1079
|
-
-- Code chunks: search by file path, name, and content
|
|
1080
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_code USING fts5(
|
|
1081
|
-
file_path,
|
|
1082
|
-
name,
|
|
1083
|
-
content,
|
|
1084
|
-
content='code_chunks',
|
|
1085
|
-
content_rowid='id',
|
|
1086
|
-
tokenize='porter unicode61'
|
|
1087
|
-
);
|
|
1088
|
-
|
|
1089
|
-
-- Git commits: search by message, author, and diff
|
|
1090
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_commits USING fts5(
|
|
1091
|
-
message,
|
|
1092
|
-
author,
|
|
1093
|
-
diff,
|
|
1094
|
-
content='git_commits',
|
|
1095
|
-
content_rowid='id',
|
|
1096
|
-
tokenize='porter unicode61'
|
|
1097
|
-
);
|
|
1098
|
-
|
|
1099
|
-
-- Memory patterns: search by task type, task, approach, and critique
|
|
1100
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_patterns USING fts5(
|
|
1101
|
-
task_type,
|
|
1102
|
-
task,
|
|
1103
|
-
approach,
|
|
1104
|
-
critique,
|
|
1105
|
-
content='memory_patterns',
|
|
1106
|
-
content_rowid='id',
|
|
1107
|
-
tokenize='porter unicode61'
|
|
1108
|
-
);
|
|
1109
|
-
|
|
1110
|
-
-- \u2500\u2500 FTS5 Sync Triggers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1111
|
-
-- Auto-sync FTS indices on INSERT/UPDATE/DELETE
|
|
1112
|
-
|
|
1113
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_code_insert AFTER INSERT ON code_chunks BEGIN
|
|
1114
|
-
INSERT INTO fts_code(rowid, file_path, name, content)
|
|
1115
|
-
VALUES (new.id, new.file_path, COALESCE(new.name, ''), new.content);
|
|
1116
|
-
END;
|
|
1117
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_code_delete AFTER DELETE ON code_chunks BEGIN
|
|
1118
|
-
INSERT INTO fts_code(fts_code, rowid, file_path, name, content)
|
|
1119
|
-
VALUES ('delete', old.id, old.file_path, COALESCE(old.name, ''), old.content);
|
|
1120
|
-
END;
|
|
1121
|
-
|
|
1122
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_commits_insert AFTER INSERT ON git_commits BEGIN
|
|
1123
|
-
INSERT INTO fts_commits(rowid, message, author, diff)
|
|
1124
|
-
VALUES (new.id, new.message, new.author, COALESCE(new.diff, ''));
|
|
1125
|
-
END;
|
|
1126
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_commits_delete AFTER DELETE ON git_commits BEGIN
|
|
1127
|
-
INSERT INTO fts_commits(fts_commits, rowid, message, author, diff)
|
|
1128
|
-
VALUES ('delete', old.id, old.message, old.author, COALESCE(old.diff, ''));
|
|
1129
|
-
END;
|
|
1130
|
-
|
|
1131
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_patterns_insert AFTER INSERT ON memory_patterns BEGIN
|
|
1132
|
-
INSERT INTO fts_patterns(rowid, task_type, task, approach, critique)
|
|
1133
|
-
VALUES (new.id, new.task_type, new.task, new.approach, COALESCE(new.critique, ''));
|
|
1134
|
-
END;
|
|
1135
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_patterns_delete AFTER DELETE ON memory_patterns BEGIN
|
|
1136
|
-
INSERT INTO fts_patterns(fts_patterns, rowid, task_type, task, approach, critique)
|
|
1137
|
-
VALUES ('delete', old.id, old.task_type, old.task, old.approach, COALESCE(old.critique, ''));
|
|
1138
|
-
END;
|
|
1139
|
-
|
|
1140
|
-
-- \u2500\u2500 Note Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1141
|
-
CREATE TABLE IF NOT EXISTS note_memories (
|
|
1142
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1143
|
-
title TEXT NOT NULL,
|
|
1144
|
-
summary TEXT NOT NULL,
|
|
1145
|
-
decisions_json TEXT NOT NULL DEFAULT '[]',
|
|
1146
|
-
files_json TEXT NOT NULL DEFAULT '[]',
|
|
1147
|
-
patterns_json TEXT NOT NULL DEFAULT '[]',
|
|
1148
|
-
open_json TEXT NOT NULL DEFAULT '[]',
|
|
1149
|
-
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
1150
|
-
tier TEXT NOT NULL DEFAULT 'short',
|
|
1151
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1152
|
-
);
|
|
1153
|
-
|
|
1154
|
-
CREATE TABLE IF NOT EXISTS note_vectors (
|
|
1155
|
-
note_id INTEGER PRIMARY KEY REFERENCES note_memories(id) ON DELETE CASCADE,
|
|
1156
|
-
embedding BLOB NOT NULL
|
|
1157
|
-
);
|
|
1158
|
-
|
|
1159
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_notes USING fts5(
|
|
1160
|
-
title,
|
|
1161
|
-
summary,
|
|
1162
|
-
decisions,
|
|
1163
|
-
patterns,
|
|
1164
|
-
tags,
|
|
1165
|
-
content='note_memories',
|
|
1166
|
-
content_rowid='id',
|
|
1167
|
-
tokenize='porter unicode61'
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_notes_insert AFTER INSERT ON note_memories BEGIN
|
|
1171
|
-
INSERT INTO fts_notes(rowid, title, summary, decisions, patterns, tags)
|
|
1172
|
-
VALUES (new.id, new.title, new.summary, new.decisions_json, new.patterns_json, new.tags_json);
|
|
1173
|
-
END;
|
|
1174
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_notes_delete AFTER DELETE ON note_memories BEGIN
|
|
1175
|
-
INSERT INTO fts_notes(fts_notes, rowid, title, summary, decisions, patterns, tags)
|
|
1176
|
-
VALUES ('delete', old.id, old.title, old.summary, old.decisions_json, old.patterns_json, old.tags_json);
|
|
1177
|
-
END;
|
|
1178
|
-
|
|
1179
|
-
CREATE INDEX IF NOT EXISTS idx_nm_tier ON note_memories(tier);
|
|
1180
|
-
CREATE INDEX IF NOT EXISTS idx_nm_created ON note_memories(created_at DESC);
|
|
1181
|
-
|
|
1182
|
-
-- \u2500\u2500 Document Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1183
|
-
CREATE TABLE IF NOT EXISTS collections (
|
|
1184
|
-
name TEXT PRIMARY KEY,
|
|
1185
|
-
path TEXT NOT NULL,
|
|
1186
|
-
pattern TEXT NOT NULL DEFAULT '**/*.md',
|
|
1187
|
-
ignore_json TEXT NOT NULL DEFAULT '[]',
|
|
1188
|
-
context TEXT,
|
|
1189
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1190
|
-
);
|
|
1191
|
-
|
|
1192
|
-
CREATE TABLE IF NOT EXISTS doc_chunks (
|
|
1193
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1194
|
-
collection TEXT NOT NULL REFERENCES collections(name) ON DELETE CASCADE,
|
|
1195
|
-
file_path TEXT NOT NULL,
|
|
1196
|
-
title TEXT NOT NULL,
|
|
1197
|
-
content TEXT NOT NULL,
|
|
1198
|
-
seq INTEGER NOT NULL DEFAULT 0,
|
|
1199
|
-
pos INTEGER NOT NULL DEFAULT 0,
|
|
1200
|
-
content_hash TEXT NOT NULL,
|
|
1201
|
-
indexed_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1202
|
-
);
|
|
1203
|
-
|
|
1204
|
-
CREATE TABLE IF NOT EXISTS doc_vectors (
|
|
1205
|
-
chunk_id INTEGER PRIMARY KEY REFERENCES doc_chunks(id) ON DELETE CASCADE,
|
|
1206
|
-
embedding BLOB NOT NULL
|
|
1207
|
-
);
|
|
1208
|
-
|
|
1209
|
-
CREATE INDEX IF NOT EXISTS idx_dc_collection ON doc_chunks(collection);
|
|
1210
|
-
CREATE INDEX IF NOT EXISTS idx_dc_file ON doc_chunks(file_path);
|
|
1211
|
-
CREATE INDEX IF NOT EXISTS idx_dc_hash ON doc_chunks(content_hash);
|
|
1212
|
-
|
|
1213
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_docs USING fts5(
|
|
1214
|
-
title,
|
|
1215
|
-
content,
|
|
1216
|
-
file_path,
|
|
1217
|
-
collection,
|
|
1218
|
-
content='doc_chunks',
|
|
1219
|
-
content_rowid='id',
|
|
1220
|
-
tokenize='porter unicode61'
|
|
1221
|
-
);
|
|
1222
|
-
|
|
1223
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_docs_insert AFTER INSERT ON doc_chunks BEGIN
|
|
1224
|
-
INSERT INTO fts_docs(rowid, title, content, file_path, collection)
|
|
1225
|
-
VALUES (new.id, new.title, new.content, new.file_path, new.collection);
|
|
1226
|
-
END;
|
|
1227
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_docs_delete AFTER DELETE ON doc_chunks BEGIN
|
|
1228
|
-
INSERT INTO fts_docs(fts_docs, rowid, title, content, file_path, collection)
|
|
1229
|
-
VALUES ('delete', old.id, old.title, old.content, old.file_path, old.collection);
|
|
1230
|
-
END;
|
|
1231
|
-
|
|
1232
|
-
-- \u2500\u2500 Path Contexts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1233
|
-
CREATE TABLE IF NOT EXISTS path_contexts (
|
|
1234
|
-
collection TEXT NOT NULL,
|
|
1235
|
-
path TEXT NOT NULL,
|
|
1236
|
-
context TEXT NOT NULL,
|
|
1237
|
-
PRIMARY KEY (collection, path)
|
|
1238
|
-
);
|
|
1239
|
-
|
|
1240
|
-
-- \u2500\u2500 Dynamic Collections (KV Store) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1241
|
-
CREATE TABLE IF NOT EXISTS kv_data (
|
|
1242
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1243
|
-
collection TEXT NOT NULL,
|
|
1244
|
-
content TEXT NOT NULL,
|
|
1245
|
-
meta_json TEXT NOT NULL DEFAULT '{}',
|
|
1246
|
-
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
1247
|
-
expires_at INTEGER,
|
|
1248
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1249
|
-
);
|
|
1250
|
-
|
|
1251
|
-
CREATE TABLE IF NOT EXISTS kv_vectors (
|
|
1252
|
-
data_id INTEGER PRIMARY KEY REFERENCES kv_data(id) ON DELETE CASCADE,
|
|
1253
|
-
embedding BLOB NOT NULL
|
|
1254
|
-
);
|
|
1255
|
-
|
|
1256
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS fts_kv USING fts5(
|
|
1257
|
-
content,
|
|
1258
|
-
collection,
|
|
1259
|
-
content='kv_data',
|
|
1260
|
-
content_rowid='id',
|
|
1261
|
-
tokenize='porter unicode61'
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1264
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_kv_insert AFTER INSERT ON kv_data BEGIN
|
|
1265
|
-
INSERT INTO fts_kv(rowid, content, collection)
|
|
1266
|
-
VALUES (new.id, new.content, new.collection);
|
|
1267
|
-
END;
|
|
1268
|
-
CREATE TRIGGER IF NOT EXISTS trg_fts_kv_delete AFTER DELETE ON kv_data BEGIN
|
|
1269
|
-
INSERT INTO fts_kv(fts_kv, rowid, content, collection)
|
|
1270
|
-
VALUES ('delete', old.id, old.content, old.collection);
|
|
1271
|
-
END;
|
|
1272
|
-
|
|
1273
|
-
CREATE INDEX IF NOT EXISTS idx_kv_collection ON kv_data(collection);
|
|
1274
|
-
CREATE INDEX IF NOT EXISTS idx_kv_created ON kv_data(created_at DESC);
|
|
1275
|
-
|
|
1276
|
-
-- \u2500\u2500 Embedding Metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1277
|
-
CREATE TABLE IF NOT EXISTS embedding_meta (
|
|
1278
|
-
key TEXT PRIMARY KEY,
|
|
1279
|
-
value TEXT NOT NULL
|
|
1280
|
-
);
|
|
1281
|
-
`);
|
|
1282
|
-
}
|
|
1283
|
-
__name(createSchema, "createSchema");
|
|
1284
|
-
|
|
1285
|
-
// src/db/database.ts
|
|
1286
|
-
var Database = class {
|
|
1287
|
-
static {
|
|
1288
|
-
__name(this, "Database");
|
|
1289
|
-
}
|
|
1290
|
-
db;
|
|
1291
|
-
constructor(dbPath) {
|
|
1292
|
-
const dir = path2.dirname(dbPath);
|
|
1293
|
-
if (!fs.existsSync(dir)) {
|
|
1294
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1295
|
-
}
|
|
1296
|
-
this.db = new BetterSqlite3(dbPath);
|
|
1297
|
-
this.db.pragma("journal_mode = WAL");
|
|
1298
|
-
this.db.pragma("busy_timeout = 5000");
|
|
1299
|
-
this.db.pragma("synchronous = NORMAL");
|
|
1300
|
-
this.db.pragma("foreign_keys = ON");
|
|
1301
|
-
createSchema(this.db);
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Run a function inside a transaction.
|
|
1305
|
-
* Auto-commits on success, auto-rollbacks on error.
|
|
1306
|
-
*/
|
|
1307
|
-
transaction(fn) {
|
|
1308
|
-
const tx = this.db.transaction(fn);
|
|
1309
|
-
return tx();
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* Run a prepared statement on multiple rows.
|
|
1313
|
-
* Wraps in a single transaction for performance.
|
|
1314
|
-
*/
|
|
1315
|
-
batch(sql, rows) {
|
|
1316
|
-
const stmt = this.db.prepare(sql);
|
|
1317
|
-
const tx = this.db.transaction(() => {
|
|
1318
|
-
for (const row of rows) {
|
|
1319
|
-
stmt.run(...row);
|
|
1320
|
-
}
|
|
1321
|
-
});
|
|
1322
|
-
tx();
|
|
1323
|
-
}
|
|
1324
|
-
/** Prepare a reusable statement. */
|
|
1325
|
-
prepare(sql) {
|
|
1326
|
-
return this.db.prepare(sql);
|
|
1327
|
-
}
|
|
1328
|
-
/** Execute raw SQL (no results). */
|
|
1329
|
-
exec(sql) {
|
|
1330
|
-
this.db.exec(sql);
|
|
1331
|
-
}
|
|
1332
|
-
/** Close the database. */
|
|
1333
|
-
close() {
|
|
1334
|
-
this.db.close();
|
|
1335
|
-
}
|
|
1336
|
-
};
|
|
1337
|
-
|
|
1338
|
-
// src/services/embedding-meta.ts
|
|
1339
|
-
function getEmbeddingMeta(db) {
|
|
1340
|
-
try {
|
|
1341
|
-
const provider = db.prepare(
|
|
1342
|
-
"SELECT value FROM embedding_meta WHERE key = 'provider'"
|
|
1343
|
-
).get();
|
|
1344
|
-
const dims = db.prepare(
|
|
1345
|
-
"SELECT value FROM embedding_meta WHERE key = 'dims'"
|
|
1346
|
-
).get();
|
|
1347
|
-
const key = db.prepare(
|
|
1348
|
-
"SELECT value FROM embedding_meta WHERE key = 'provider_key'"
|
|
1349
|
-
).get();
|
|
1350
|
-
if (!provider || !dims) return null;
|
|
1351
|
-
return {
|
|
1352
|
-
provider: provider.value,
|
|
1353
|
-
dims: Number(dims.value),
|
|
1354
|
-
providerKey: key?.value ?? "local"
|
|
1355
|
-
};
|
|
1356
|
-
} catch {
|
|
1357
|
-
return null;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
__name(getEmbeddingMeta, "getEmbeddingMeta");
|
|
1361
|
-
function setEmbeddingMeta(db, embedding) {
|
|
1362
|
-
const upsert = db.prepare(
|
|
1363
|
-
"INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)"
|
|
1364
|
-
);
|
|
1365
|
-
upsert.run("provider", embedding.constructor?.name ?? "unknown");
|
|
1366
|
-
upsert.run("dims", String(embedding.dims));
|
|
1367
|
-
upsert.run("provider_key", providerKey(embedding));
|
|
1368
|
-
upsert.run("indexed_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
1369
|
-
}
|
|
1370
|
-
__name(setEmbeddingMeta, "setEmbeddingMeta");
|
|
1371
|
-
function detectProviderMismatch(db, embedding) {
|
|
1372
|
-
const meta = getEmbeddingMeta(db);
|
|
1373
|
-
if (!meta) return null;
|
|
1374
|
-
const currentName = embedding.constructor?.name ?? "unknown";
|
|
1375
|
-
const mismatch = meta.dims !== embedding.dims || meta.provider !== currentName;
|
|
1376
|
-
return {
|
|
1377
|
-
mismatch,
|
|
1378
|
-
stored: `${meta.provider}/${meta.dims}`,
|
|
1379
|
-
current: `${currentName}/${embedding.dims}`
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
__name(detectProviderMismatch, "detectProviderMismatch");
|
|
1383
|
-
|
|
1384
|
-
// src/bootstrap/initializer.ts
|
|
1385
|
-
var Initializer = class {
|
|
1386
|
-
static {
|
|
1387
|
-
__name(this, "Initializer");
|
|
1388
|
-
}
|
|
1389
|
-
_config;
|
|
1390
|
-
_emit;
|
|
1391
|
-
constructor(config, emit) {
|
|
1392
|
-
this._config = config;
|
|
1393
|
-
this._emit = emit;
|
|
1394
|
-
}
|
|
1395
|
-
/** Phase 1: database, embedding provider, KV HNSW index. */
|
|
1396
|
-
async early(options = {}) {
|
|
1397
|
-
const { _config: config } = this;
|
|
1398
|
-
const db = new Database(config.dbPath);
|
|
1399
|
-
const embedding = await this._resolveEmbedding(db);
|
|
1400
|
-
const mismatch = detectProviderMismatch(db, embedding);
|
|
1401
|
-
if (mismatch?.mismatch && !options.force) {
|
|
1402
|
-
db.close();
|
|
1403
|
-
throw new Error(
|
|
1404
|
-
`BrainBank: Embedding dimension mismatch (stored: ${mismatch.stored}, current: ${mismatch.current}). Run brain.reembed() to re-index with the new provider, or switch back to the original provider.`
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
setEmbeddingMeta(db, embedding);
|
|
1408
|
-
const kvHnsw = new HNSWIndex(
|
|
1409
|
-
config.embeddingDims,
|
|
1410
|
-
config.maxElements ?? 5e5,
|
|
1411
|
-
config.hnswM,
|
|
1412
|
-
config.hnswEfConstruction,
|
|
1413
|
-
config.hnswEfSearch
|
|
1414
|
-
);
|
|
1415
|
-
await kvHnsw.init();
|
|
1416
|
-
const skipVectorLoad = !!(options.force && mismatch?.mismatch);
|
|
1417
|
-
return { db, embedding, kvHnsw, skipVectorLoad };
|
|
1418
|
-
}
|
|
1419
|
-
/** Phase 2: load vectors, run indexers, build search layer. */
|
|
1420
|
-
async late(earlyResult, registry, sharedHnsw, kvVecs, getCollection) {
|
|
1421
|
-
const { _config: config } = this;
|
|
1422
|
-
const { db, embedding, kvHnsw, skipVectorLoad } = earlyResult;
|
|
1423
|
-
if (!skipVectorLoad) {
|
|
1424
|
-
const kvIndexPath = hnswPath(config.dbPath, "kv");
|
|
1425
|
-
const kvCount = countRows(db, "kv_vectors");
|
|
1426
|
-
if (kvHnsw.tryLoad(kvIndexPath, kvCount)) {
|
|
1427
|
-
loadVecCache(db, "kv_vectors", "data_id", kvVecs);
|
|
1428
|
-
} else {
|
|
1429
|
-
loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
const ctx = this._buildPluginContext(db, embedding, sharedHnsw, skipVectorLoad, getCollection);
|
|
1433
|
-
for (const mod of registry.all) {
|
|
1434
|
-
await mod.initialize(ctx);
|
|
1435
|
-
}
|
|
1436
|
-
saveAllHnsw(config.dbPath, kvHnsw, sharedHnsw);
|
|
1437
|
-
return this._buildSearchLayer(db, embedding, registry, sharedHnsw);
|
|
1438
|
-
}
|
|
1439
|
-
/** Build the PluginContext passed to each plugin's initialize(). */
|
|
1440
|
-
_buildPluginContext(db, embedding, sharedHnsw, skipVectorLoad, getCollection) {
|
|
1441
|
-
const { _config: config } = this;
|
|
1442
|
-
return {
|
|
1443
|
-
db,
|
|
1444
|
-
embedding,
|
|
1445
|
-
config,
|
|
1446
|
-
createHnsw: /* @__PURE__ */ __name((maxElements, dims) => new HNSWIndex(
|
|
1447
|
-
dims ?? config.embeddingDims,
|
|
1448
|
-
maxElements ?? config.maxElements,
|
|
1449
|
-
config.hnswM,
|
|
1450
|
-
config.hnswEfConstruction,
|
|
1451
|
-
config.hnswEfSearch
|
|
1452
|
-
).init(), "createHnsw"),
|
|
1453
|
-
loadVectors: /* @__PURE__ */ __name((table, idCol, hnsw, cache) => {
|
|
1454
|
-
if (skipVectorLoad) return;
|
|
1455
|
-
const indexName = table.replace("_vectors", "").replace("_chunks", "");
|
|
1456
|
-
const indexPath = hnswPath(config.dbPath, indexName);
|
|
1457
|
-
const rowCount = countRows(db, table);
|
|
1458
|
-
if (hnsw.tryLoad(indexPath, rowCount)) {
|
|
1459
|
-
loadVecCache(db, table, idCol, cache);
|
|
1460
|
-
} else {
|
|
1461
|
-
loadVectors(db, table, idCol, hnsw, cache);
|
|
1462
|
-
}
|
|
1463
|
-
}, "loadVectors"),
|
|
1464
|
-
getOrCreateSharedHnsw: /* @__PURE__ */ __name(async (type, maxElements, dims) => {
|
|
1465
|
-
const existing = sharedHnsw.get(type);
|
|
1466
|
-
if (existing) return { ...existing, isNew: false };
|
|
1467
|
-
const hnswDims = dims ?? config.embeddingDims;
|
|
1468
|
-
const hnsw = await new HNSWIndex(
|
|
1469
|
-
hnswDims,
|
|
1470
|
-
maxElements ?? config.maxElements,
|
|
1471
|
-
config.hnswM,
|
|
1472
|
-
config.hnswEfConstruction,
|
|
1473
|
-
config.hnswEfSearch
|
|
1474
|
-
).init();
|
|
1475
|
-
const vecCache = /* @__PURE__ */ new Map();
|
|
1476
|
-
sharedHnsw.set(type, { hnsw, vecCache });
|
|
1477
|
-
return { hnsw, vecCache, isNew: true };
|
|
1478
|
-
}, "getOrCreateSharedHnsw"),
|
|
1479
|
-
collection: getCollection
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
/** Build VectorSearch + KeywordSearch + ContextBuilder from initialized plugins. */
|
|
1483
|
-
_buildSearchLayer(db, embedding, registry, sharedHnsw) {
|
|
1484
|
-
const { _config: config } = this;
|
|
1485
|
-
const codeMod = sharedHnsw.get("code");
|
|
1486
|
-
const gitMod = sharedHnsw.get("git");
|
|
1487
|
-
const memMod = registry.firstByType("memory");
|
|
1488
|
-
if (!codeMod && !gitMod && !memMod) return {};
|
|
1489
|
-
const search = new VectorSearch({
|
|
1490
|
-
db,
|
|
1491
|
-
codeHnsw: codeMod?.hnsw,
|
|
1492
|
-
gitHnsw: gitMod?.hnsw,
|
|
1493
|
-
patternHnsw: memMod?.hnsw,
|
|
1494
|
-
codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1495
|
-
gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1496
|
-
patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1497
|
-
embedding,
|
|
1498
|
-
reranker: config.reranker
|
|
1499
|
-
});
|
|
1500
|
-
const bm25 = new KeywordSearch(db);
|
|
1501
|
-
const firstGit = registry.firstByType("git");
|
|
1502
|
-
const contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
|
|
1503
|
-
return { search, bm25, contextBuilder };
|
|
1504
|
-
}
|
|
1505
|
-
/** Resolve embedding: explicit config > stored DB key > local default. */
|
|
1506
|
-
async _resolveEmbedding(db) {
|
|
1507
|
-
if (this._config.embeddingProvider) return this._config.embeddingProvider;
|
|
1508
|
-
const meta = getEmbeddingMeta(db);
|
|
1509
|
-
if (meta?.providerKey && meta.providerKey !== "local") {
|
|
1510
|
-
this._emit("progress", `Embedding: auto-resolved '${meta.providerKey}' from DB`);
|
|
1511
|
-
return resolveEmbedding(meta.providerKey);
|
|
1512
|
-
}
|
|
1513
|
-
return resolveEmbedding("local");
|
|
1514
|
-
}
|
|
1515
|
-
};
|
|
1516
|
-
function hnswPath(dbPath, name) {
|
|
1517
|
-
return join2(dirname2(dbPath), `hnsw-${name}.index`);
|
|
1518
|
-
}
|
|
1519
|
-
__name(hnswPath, "hnswPath");
|
|
1520
|
-
function countRows(db, table) {
|
|
1521
|
-
const row = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get();
|
|
1522
|
-
return row?.c ?? 0;
|
|
1523
|
-
}
|
|
1524
|
-
__name(countRows, "countRows");
|
|
1525
|
-
function saveAllHnsw(dbPath, kvHnsw, sharedHnsw) {
|
|
1526
|
-
try {
|
|
1527
|
-
kvHnsw.save(hnswPath(dbPath, "kv"));
|
|
1528
|
-
for (const [name, { hnsw }] of sharedHnsw) {
|
|
1529
|
-
hnsw.save(hnswPath(dbPath, name));
|
|
1530
|
-
}
|
|
1531
|
-
} catch {
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
__name(saveAllHnsw, "saveAllHnsw");
|
|
1535
|
-
function loadVectors(db, table, idCol, hnsw, cache) {
|
|
1536
|
-
const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
|
|
1537
|
-
for (const row of iter) {
|
|
1538
|
-
const vec = new Float32Array(
|
|
1539
|
-
row.embedding.buffer.slice(
|
|
1540
|
-
row.embedding.byteOffset,
|
|
1541
|
-
row.embedding.byteOffset + row.embedding.byteLength
|
|
1542
|
-
)
|
|
1543
|
-
);
|
|
1544
|
-
hnsw.add(vec, row[idCol]);
|
|
1545
|
-
cache.set(row[idCol], vec);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
__name(loadVectors, "loadVectors");
|
|
1549
|
-
function loadVecCache(db, table, idCol, cache) {
|
|
1550
|
-
const iter = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).iterate();
|
|
1551
|
-
for (const row of iter) {
|
|
1552
|
-
const vec = new Float32Array(
|
|
1553
|
-
row.embedding.buffer.slice(
|
|
1554
|
-
row.embedding.byteOffset,
|
|
1555
|
-
row.embedding.byteOffset + row.embedding.byteLength
|
|
1556
|
-
)
|
|
1557
|
-
);
|
|
1558
|
-
cache.set(row[idCol], vec);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
__name(loadVecCache, "loadVecCache");
|
|
1562
|
-
|
|
1563
|
-
// src/api/search-api.ts
|
|
1564
|
-
var SearchAPI = class {
|
|
1565
|
-
constructor(_d) {
|
|
1566
|
-
this._d = _d;
|
|
1567
|
-
}
|
|
1568
|
-
static {
|
|
1569
|
-
__name(this, "SearchAPI");
|
|
1570
|
-
}
|
|
1571
|
-
// ── Vector ──────────────────────────────────────
|
|
1572
|
-
async search(query, options) {
|
|
1573
|
-
if (!this._d.search) {
|
|
1574
|
-
return this._d.registry.has("docs") ? await this._searchDocs(query, { k: 8 }) : [];
|
|
1575
|
-
}
|
|
1576
|
-
return this._d.search.search(query, options);
|
|
1577
|
-
}
|
|
1578
|
-
async searchCode(query, k = 8) {
|
|
1579
|
-
if (!this._d.registry.firstByType("code"))
|
|
1580
|
-
throw new Error("BrainBank: Indexer 'code' is not loaded. Add .use(code()) to your BrainBank instance.");
|
|
1581
|
-
if (!this._d.search)
|
|
1582
|
-
throw new Error("BrainBank: MultiIndexSearch not available. Ensure code indexer is loaded.");
|
|
1583
|
-
return this._d.search.search(query, { codeK: k, gitK: 0, patternK: 0 });
|
|
1584
|
-
}
|
|
1585
|
-
async searchCommits(query, k = 8) {
|
|
1586
|
-
if (!this._d.registry.firstByType("git"))
|
|
1587
|
-
throw new Error("BrainBank: Indexer 'git' is not loaded. Add .use(git()) to your BrainBank instance.");
|
|
1588
|
-
if (!this._d.search)
|
|
1589
|
-
throw new Error("BrainBank: MultiIndexSearch not available. Ensure git indexer is loaded.");
|
|
1590
|
-
return this._d.search.search(query, { codeK: 0, gitK: k, patternK: 0 });
|
|
1591
|
-
}
|
|
1592
|
-
// ── Hybrid ──────────────────────────────────────
|
|
1593
|
-
async hybridSearch(query, options) {
|
|
1594
|
-
const cols = options?.collections ?? {};
|
|
1595
|
-
const codeK = cols.code ?? options?.codeK ?? 6;
|
|
1596
|
-
const gitK = cols.git ?? options?.gitK ?? 5;
|
|
1597
|
-
const docsK = cols.docs ?? 8;
|
|
1598
|
-
const resultLists = [];
|
|
1599
|
-
if (this._d.search) {
|
|
1600
|
-
const [vec, kw] = await Promise.all([
|
|
1601
|
-
this._d.search.search(query, { ...options, codeK, gitK }),
|
|
1602
|
-
Promise.resolve(this._d.bm25?.search(query, { codeK, gitK }) ?? [])
|
|
1603
|
-
]);
|
|
1604
|
-
resultLists.push(vec, kw);
|
|
1605
|
-
}
|
|
1606
|
-
if (this._d.registry.has("docs")) {
|
|
1607
|
-
const docs = await this._searchDocs(query, { k: docsK });
|
|
1608
|
-
if (docs.length > 0) resultLists.push(docs);
|
|
1609
|
-
}
|
|
1610
|
-
await this._searchKvCollections(query, cols, resultLists);
|
|
1611
|
-
if (resultLists.length === 0) return [];
|
|
1612
|
-
const fused = reciprocalRankFusion(resultLists);
|
|
1613
|
-
return this._rerankResults(query, fused);
|
|
1614
|
-
}
|
|
1615
|
-
/** Search non-reserved KV collections and push results. */
|
|
1616
|
-
async _searchKvCollections(query, cols, resultLists) {
|
|
1617
|
-
const reserved = /* @__PURE__ */ new Set(["code", "git", "docs"]);
|
|
1618
|
-
for (const [name, k] of Object.entries(cols)) {
|
|
1619
|
-
if (reserved.has(name)) continue;
|
|
1620
|
-
const hits = await this._d.collection(name).search(query, { k });
|
|
1621
|
-
if (hits.length > 0) {
|
|
1622
|
-
resultLists.push(hits.map((h) => ({
|
|
1623
|
-
type: "collection",
|
|
1624
|
-
score: h.score ?? 0,
|
|
1625
|
-
content: h.content,
|
|
1626
|
-
metadata: { collection: name, id: h.id, ...h.metadata }
|
|
1627
|
-
})));
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
/** Apply reranking if a reranker is configured. */
|
|
1632
|
-
async _rerankResults(query, fused) {
|
|
1633
|
-
if (!this._d.config.reranker || fused.length <= 1) return fused;
|
|
1634
|
-
return rerank(query, fused, this._d.config.reranker);
|
|
1635
|
-
}
|
|
1636
|
-
/** Search docs directly via the plugin — no circular callback. */
|
|
1637
|
-
async _searchDocs(query, options) {
|
|
1638
|
-
const plugin = this._d.getDocsPlugin();
|
|
1639
|
-
if (!plugin) return [];
|
|
1640
|
-
return plugin.search(query, options);
|
|
1641
|
-
}
|
|
1642
|
-
// ── Keyword ─────────────────────────────────────
|
|
1643
|
-
async searchBM25(query, options) {
|
|
1644
|
-
return this._d.bm25?.search(query, options) ?? [];
|
|
1645
|
-
}
|
|
1646
|
-
rebuildFTS() {
|
|
1647
|
-
this._d.bm25?.rebuild?.();
|
|
1648
|
-
}
|
|
1649
|
-
// ── Context ─────────────────────────────────────
|
|
1650
|
-
async getContext(task, options = {}) {
|
|
1651
|
-
const sections = [];
|
|
1652
|
-
if (this._d.contextBuilder) {
|
|
1653
|
-
const core = await this._d.contextBuilder.build(task, options);
|
|
1654
|
-
if (core) sections.push(core);
|
|
1655
|
-
}
|
|
1656
|
-
if (this._d.registry.has("docs")) {
|
|
1657
|
-
const docs = await this._searchDocs(task, { k: options.codeResults ?? 4 });
|
|
1658
|
-
if (docs.length > 0) {
|
|
1659
|
-
const body = docs.map((r) => {
|
|
1660
|
-
const m = r.metadata;
|
|
1661
|
-
const h = r.context ? `**[${m.collection}]** ${m.title} \u2014 _${r.context}_` : `**[${m.collection}]** ${m.title}`;
|
|
1662
|
-
return `${h}
|
|
1663
|
-
|
|
1664
|
-
${r.content}`;
|
|
1665
|
-
}).join("\n\n---\n\n");
|
|
1666
|
-
sections.push(`## Relevant Documents
|
|
1667
|
-
|
|
1668
|
-
${body}`);
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
return sections.join("\n\n");
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
// src/indexers/base.ts
|
|
1676
|
-
function isIndexable(i) {
|
|
1677
|
-
return typeof i.index === "function";
|
|
1678
|
-
}
|
|
1679
|
-
__name(isIndexable, "isIndexable");
|
|
1680
|
-
function isWatchable(i) {
|
|
1681
|
-
return typeof i.onFileChange === "function" && typeof i.watchPatterns === "function";
|
|
1682
|
-
}
|
|
1683
|
-
__name(isWatchable, "isWatchable");
|
|
1684
|
-
function isCollectionPlugin(i) {
|
|
1685
|
-
return typeof i.addCollection === "function" && typeof i.listCollections === "function";
|
|
1686
|
-
}
|
|
1687
|
-
__name(isCollectionPlugin, "isCollectionPlugin");
|
|
1688
|
-
|
|
1689
|
-
// src/api/index-api.ts
|
|
1690
|
-
var IndexAPI = class {
|
|
1691
|
-
constructor(_d) {
|
|
1692
|
-
this._d = _d;
|
|
1693
|
-
}
|
|
1694
|
-
static {
|
|
1695
|
-
__name(this, "IndexAPI");
|
|
1696
|
-
}
|
|
1697
|
-
async index(options = {}) {
|
|
1698
|
-
const want = new Set(options.modules ?? ["code", "git", "docs"]);
|
|
1699
|
-
const result = {};
|
|
1700
|
-
if (want.has("code")) {
|
|
1701
|
-
for (const mod of this._d.registry.allByType("code")) {
|
|
1702
|
-
if (!isIndexable(mod)) continue;
|
|
1703
|
-
const label = mod.name === "code" ? "code" : mod.name;
|
|
1704
|
-
options.onProgress?.(label, "Starting...");
|
|
1705
|
-
const r = await mod.index({
|
|
1706
|
-
forceReindex: options.forceReindex,
|
|
1707
|
-
onProgress: /* @__PURE__ */ __name((f, i, t) => options.onProgress?.(label, `[${i}/${t}] ${f}`), "onProgress")
|
|
1708
|
-
});
|
|
1709
|
-
if (result.code) {
|
|
1710
|
-
result.code.indexed += r.indexed;
|
|
1711
|
-
result.code.skipped += r.skipped;
|
|
1712
|
-
result.code.chunks = (result.code.chunks ?? 0) + (r.chunks ?? 0);
|
|
1713
|
-
} else {
|
|
1714
|
-
result.code = r;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
if (want.has("git")) {
|
|
1719
|
-
for (const mod of this._d.registry.allByType("git")) {
|
|
1720
|
-
if (!isIndexable(mod)) continue;
|
|
1721
|
-
const label = mod.name === "git" ? "git" : mod.name;
|
|
1722
|
-
options.onProgress?.(label, "Starting...");
|
|
1723
|
-
const r = await mod.index({
|
|
1724
|
-
depth: options.gitDepth ?? this._d.gitDepth,
|
|
1725
|
-
onProgress: /* @__PURE__ */ __name((f, i, t) => options.onProgress?.(label, `[${i}/${t}] ${f}`), "onProgress")
|
|
1726
|
-
});
|
|
1727
|
-
if (result.git) {
|
|
1728
|
-
result.git.indexed += r.indexed;
|
|
1729
|
-
result.git.skipped += r.skipped;
|
|
1730
|
-
} else {
|
|
1731
|
-
result.git = r;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
if (want.has("docs") && this._d.registry.has("docs")) {
|
|
1736
|
-
const docsPlugin = this._d.registry.get("docs");
|
|
1737
|
-
if (isCollectionPlugin(docsPlugin)) {
|
|
1738
|
-
options.onProgress?.("docs", "Starting...");
|
|
1739
|
-
result.docs = await docsPlugin.indexCollections({
|
|
1740
|
-
onProgress: /* @__PURE__ */ __name((coll, file, cur, total) => options.onProgress?.("docs", `[${coll}] ${cur}/${total}: ${file}`), "onProgress")
|
|
1741
|
-
});
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
this._d.emit("indexed", result);
|
|
1745
|
-
return result;
|
|
1746
|
-
}
|
|
1747
|
-
async indexCode(options = {}) {
|
|
1748
|
-
const mods = this._d.registry.allByType("code").filter(isIndexable);
|
|
1749
|
-
if (!mods.length) throw new Error("BrainBank: Indexer 'code' is not loaded. Add .use(code()) to your BrainBank instance.");
|
|
1750
|
-
const acc = { indexed: 0, skipped: 0, chunks: 0 };
|
|
1751
|
-
for (const mod of mods) {
|
|
1752
|
-
const r = await mod.index(options);
|
|
1753
|
-
acc.indexed += r.indexed;
|
|
1754
|
-
acc.skipped += r.skipped;
|
|
1755
|
-
acc.chunks = (acc.chunks ?? 0) + (r.chunks ?? 0);
|
|
1756
|
-
}
|
|
1757
|
-
return acc;
|
|
1758
|
-
}
|
|
1759
|
-
async indexGit(options = {}) {
|
|
1760
|
-
const mods = this._d.registry.allByType("git").filter(isIndexable);
|
|
1761
|
-
if (!mods.length) throw new Error("BrainBank: Indexer 'git' is not loaded. Add .use(git()) to your BrainBank instance.");
|
|
1762
|
-
const acc = { indexed: 0, skipped: 0 };
|
|
1763
|
-
for (const mod of mods) {
|
|
1764
|
-
const r = await mod.index(options);
|
|
1765
|
-
acc.indexed += r.indexed;
|
|
1766
|
-
acc.skipped += r.skipped;
|
|
1767
|
-
}
|
|
1768
|
-
return acc;
|
|
1769
|
-
}
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
// src/services/reembed.ts
|
|
1773
|
-
var TABLES = [
|
|
1774
|
-
{
|
|
1775
|
-
name: "code",
|
|
1776
|
-
textTable: "code_chunks",
|
|
1777
|
-
vectorTable: "code_vectors",
|
|
1778
|
-
idColumn: "id",
|
|
1779
|
-
fkColumn: "chunk_id",
|
|
1780
|
-
textBuilder: /* @__PURE__ */ __name((r) => [
|
|
1781
|
-
`File: ${r.file_path}`,
|
|
1782
|
-
r.name ? `${r.chunk_type}: ${r.name}` : r.chunk_type,
|
|
1783
|
-
r.content
|
|
1784
|
-
].join("\n"), "textBuilder")
|
|
1785
|
-
},
|
|
1786
|
-
{
|
|
1787
|
-
name: "git",
|
|
1788
|
-
textTable: "git_commits",
|
|
1789
|
-
vectorTable: "git_vectors",
|
|
1790
|
-
idColumn: "id",
|
|
1791
|
-
fkColumn: "commit_id",
|
|
1792
|
-
// Must match git-engine.ts:119-125 exactly
|
|
1793
|
-
textBuilder: /* @__PURE__ */ __name((r) => [
|
|
1794
|
-
`Commit: ${r.message}`,
|
|
1795
|
-
`Author: ${r.author}`,
|
|
1796
|
-
`Date: ${r.date}`,
|
|
1797
|
-
r.files_json && r.files_json !== "[]" ? `Files: ${JSON.parse(r.files_json).join(", ")}` : "",
|
|
1798
|
-
r.diff ? `Changes:
|
|
1799
|
-
${r.diff.slice(0, 2e3)}` : ""
|
|
1800
|
-
].filter(Boolean).join("\n"), "textBuilder")
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
name: "memory",
|
|
1804
|
-
textTable: "memory_patterns",
|
|
1805
|
-
vectorTable: "memory_vectors",
|
|
1806
|
-
idColumn: "id",
|
|
1807
|
-
fkColumn: "pattern_id",
|
|
1808
|
-
// Must match memory/pattern-store.ts:49 exactly
|
|
1809
|
-
textBuilder: /* @__PURE__ */ __name((r) => `${r.task_type} ${r.task} ${r.approach}`, "textBuilder")
|
|
1810
|
-
},
|
|
1811
|
-
{
|
|
1812
|
-
name: "notes",
|
|
1813
|
-
textTable: "note_memories",
|
|
1814
|
-
vectorTable: "note_vectors",
|
|
1815
|
-
idColumn: "id",
|
|
1816
|
-
fkColumn: "note_id",
|
|
1817
|
-
// Must match notes/engine.ts:90 exactly
|
|
1818
|
-
textBuilder: /* @__PURE__ */ __name((r) => {
|
|
1819
|
-
const decisions = JSON.parse(r.decisions_json || "[]").join(". ");
|
|
1820
|
-
const patterns = JSON.parse(r.patterns_json || "[]").join(". ");
|
|
1821
|
-
return `${r.title}
|
|
1822
|
-
${r.summary}
|
|
1823
|
-
${decisions}
|
|
1824
|
-
${patterns}`;
|
|
1825
|
-
}, "textBuilder")
|
|
1826
|
-
},
|
|
1827
|
-
{
|
|
1828
|
-
name: "docs",
|
|
1829
|
-
textTable: "doc_chunks",
|
|
1830
|
-
vectorTable: "doc_vectors",
|
|
1831
|
-
idColumn: "id",
|
|
1832
|
-
fkColumn: "chunk_id",
|
|
1833
|
-
// Must match docs-engine.ts:160 exactly
|
|
1834
|
-
textBuilder: /* @__PURE__ */ __name((r) => `title: ${r.title ?? ""} | text: ${r.content}`, "textBuilder")
|
|
1835
|
-
},
|
|
1836
|
-
{
|
|
1837
|
-
name: "kv",
|
|
1838
|
-
textTable: "kv_data",
|
|
1839
|
-
vectorTable: "kv_vectors",
|
|
1840
|
-
idColumn: "id",
|
|
1841
|
-
fkColumn: "data_id",
|
|
1842
|
-
textBuilder: /* @__PURE__ */ __name((r) => r.content, "textBuilder")
|
|
1843
|
-
}
|
|
1844
|
-
];
|
|
1845
|
-
async function reembedAll(db, embedding, hnswMap, options = {}) {
|
|
1846
|
-
const { batchSize = 50, onProgress } = options;
|
|
1847
|
-
const result = {};
|
|
1848
|
-
let total = 0;
|
|
1849
|
-
for (const table of TABLES) {
|
|
1850
|
-
const count = await reembedTable(db, embedding, table, batchSize, onProgress);
|
|
1851
|
-
result[table.name] = count;
|
|
1852
|
-
total += count;
|
|
1853
|
-
const entry = hnswMap.get(table.name);
|
|
1854
|
-
if (entry && count > 0) {
|
|
1855
|
-
await rebuildHnsw(db, table, entry.hnsw, entry.vecs);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
const meta = {
|
|
1859
|
-
provider: embedding.constructor?.name ?? "unknown",
|
|
1860
|
-
dims: String(embedding.dims),
|
|
1861
|
-
reembedded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1862
|
-
};
|
|
1863
|
-
const upsert = db.prepare(
|
|
1864
|
-
"INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)"
|
|
1865
|
-
);
|
|
1866
|
-
for (const [k, v] of Object.entries(meta)) {
|
|
1867
|
-
upsert.run(k, v);
|
|
1868
|
-
}
|
|
1869
|
-
return {
|
|
1870
|
-
code: result.code ?? 0,
|
|
1871
|
-
git: result.git ?? 0,
|
|
1872
|
-
memory: result.memory ?? 0,
|
|
1873
|
-
notes: result.notes ?? 0,
|
|
1874
|
-
docs: result.docs ?? 0,
|
|
1875
|
-
kv: result.kv ?? 0,
|
|
1876
|
-
total
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
__name(reembedAll, "reembedAll");
|
|
1880
|
-
async function reembedTable(db, embedding, table, batchSize, onProgress) {
|
|
1881
|
-
const totalCount = db.prepare(
|
|
1882
|
-
`SELECT COUNT(*) as c FROM ${table.textTable}`
|
|
1883
|
-
).get().c;
|
|
1884
|
-
if (totalCount === 0) return 0;
|
|
1885
|
-
const tempTable = `_reembed_${table.vectorTable}`;
|
|
1886
|
-
db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
|
|
1887
|
-
db.exec(`CREATE TABLE ${tempTable} AS SELECT * FROM ${table.vectorTable} WHERE 0`);
|
|
1888
|
-
const insertTemp = db.prepare(
|
|
1889
|
-
`INSERT INTO ${tempTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
|
|
1890
|
-
);
|
|
1891
|
-
let processed = 0;
|
|
1892
|
-
try {
|
|
1893
|
-
for (let offset = 0; offset < totalCount; offset += batchSize) {
|
|
1894
|
-
const batch = db.prepare(
|
|
1895
|
-
`SELECT * FROM ${table.textTable} LIMIT ? OFFSET ?`
|
|
1896
|
-
).all(batchSize, offset);
|
|
1897
|
-
const texts = batch.map((r) => table.textBuilder(r));
|
|
1898
|
-
const vectors = await embedding.embedBatch(texts);
|
|
1899
|
-
db.transaction(() => {
|
|
1900
|
-
for (let j = 0; j < batch.length; j++) {
|
|
1901
|
-
insertTemp.run(batch[j][table.idColumn], vecToBuffer(vectors[j]));
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
processed += batch.length;
|
|
1905
|
-
onProgress?.(table.name, processed, totalCount);
|
|
1906
|
-
}
|
|
1907
|
-
db.transaction(() => {
|
|
1908
|
-
db.exec(`DELETE FROM ${table.vectorTable}`);
|
|
1909
|
-
db.exec(`INSERT INTO ${table.vectorTable} SELECT * FROM ${tempTable}`);
|
|
1910
|
-
});
|
|
1911
|
-
} finally {
|
|
1912
|
-
db.exec(`DROP TABLE IF EXISTS ${tempTable}`);
|
|
1913
|
-
}
|
|
1914
|
-
return processed;
|
|
1915
|
-
}
|
|
1916
|
-
__name(reembedTable, "reembedTable");
|
|
1917
|
-
async function rebuildHnsw(db, table, hnsw, vecs) {
|
|
1918
|
-
vecs.clear();
|
|
1919
|
-
hnsw.reinit();
|
|
1920
|
-
const rows = db.prepare(
|
|
1921
|
-
`SELECT ${table.fkColumn} as id, embedding FROM ${table.vectorTable}`
|
|
1922
|
-
).all();
|
|
1923
|
-
for (const row of rows) {
|
|
1924
|
-
const buf = Buffer.from(row.embedding);
|
|
1925
|
-
const vec = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
1926
|
-
hnsw.add(vec, row.id);
|
|
1927
|
-
vecs.set(row.id, vec);
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
__name(rebuildHnsw, "rebuildHnsw");
|
|
1931
|
-
|
|
1932
|
-
// src/services/watch.ts
|
|
1933
|
-
import * as fs2 from "fs";
|
|
1934
|
-
import * as path3 from "path";
|
|
1935
|
-
function createWatcher(reindexFn, indexers, repoPath, options = {}) {
|
|
1936
|
-
const {
|
|
1937
|
-
paths = [repoPath],
|
|
1938
|
-
debounceMs = 2e3,
|
|
1939
|
-
onIndex,
|
|
1940
|
-
onError
|
|
1941
|
-
} = options;
|
|
1942
|
-
let active = true;
|
|
1943
|
-
const watchers = [];
|
|
1944
|
-
const pending = /* @__PURE__ */ new Set();
|
|
1945
|
-
let timer = null;
|
|
1946
|
-
const customPatterns = [];
|
|
1947
|
-
for (const indexer of indexers.values()) {
|
|
1948
|
-
if (isWatchable(indexer)) {
|
|
1949
|
-
customPatterns.push({ indexer, patterns: indexer.watchPatterns() });
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
function matchCustomPlugin(filePath) {
|
|
1953
|
-
const rel = path3.relative(repoPath, filePath);
|
|
1954
|
-
for (const { indexer, patterns } of customPatterns) {
|
|
1955
|
-
for (const pattern of patterns) {
|
|
1956
|
-
if (matchGlob(rel, pattern)) return indexer;
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
return null;
|
|
1960
|
-
}
|
|
1961
|
-
__name(matchCustomPlugin, "matchCustomPlugin");
|
|
1962
|
-
function matchGlob(filePath, pattern) {
|
|
1963
|
-
if (pattern.startsWith("**/")) {
|
|
1964
|
-
const suffix = pattern.slice(3);
|
|
1965
|
-
const ext = suffix.startsWith("*.") ? suffix.slice(1) : null;
|
|
1966
|
-
if (ext) return filePath.endsWith(ext);
|
|
1967
|
-
return path3.basename(filePath) === suffix;
|
|
1968
|
-
}
|
|
1969
|
-
if (pattern.startsWith("*.")) {
|
|
1970
|
-
return filePath.endsWith(pattern.slice(1));
|
|
1971
|
-
}
|
|
1972
|
-
return filePath === pattern;
|
|
1973
|
-
}
|
|
1974
|
-
__name(matchGlob, "matchGlob");
|
|
1975
|
-
let flushing = false;
|
|
1976
|
-
async function processPending() {
|
|
1977
|
-
if (flushing || pending.size === 0) return;
|
|
1978
|
-
flushing = true;
|
|
1979
|
-
try {
|
|
1980
|
-
const files = [...pending];
|
|
1981
|
-
pending.clear();
|
|
1982
|
-
let needsReindex = false;
|
|
1983
|
-
for (const filePath of files) {
|
|
1984
|
-
const absPath = path3.resolve(repoPath, filePath);
|
|
1985
|
-
const customIndexer = matchCustomPlugin(absPath);
|
|
1986
|
-
if (customIndexer && isWatchable(customIndexer)) {
|
|
1987
|
-
try {
|
|
1988
|
-
const handled = await customIndexer.onFileChange(absPath, detectEvent(absPath));
|
|
1989
|
-
if (handled) {
|
|
1990
|
-
onIndex?.(filePath, customIndexer.name);
|
|
1991
|
-
continue;
|
|
1992
|
-
}
|
|
1993
|
-
} catch (err) {
|
|
1994
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
if (isSupported(filePath)) {
|
|
1998
|
-
needsReindex = true;
|
|
1999
|
-
onIndex?.(filePath, "code");
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
if (needsReindex) {
|
|
2003
|
-
try {
|
|
2004
|
-
await reindexFn();
|
|
2005
|
-
} catch (err) {
|
|
2006
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
} finally {
|
|
2010
|
-
flushing = false;
|
|
2011
|
-
if (pending.size > 0) {
|
|
2012
|
-
timer = setTimeout(() => processPending(), debounceMs);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
__name(processPending, "processPending");
|
|
2017
|
-
function detectEvent(filePath) {
|
|
2018
|
-
try {
|
|
2019
|
-
fs2.accessSync(filePath);
|
|
2020
|
-
return "update";
|
|
2021
|
-
} catch {
|
|
2022
|
-
return "delete";
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
__name(detectEvent, "detectEvent");
|
|
2026
|
-
function shouldWatch(filename) {
|
|
2027
|
-
if (!filename) return false;
|
|
2028
|
-
const parts = filename.split(path3.sep);
|
|
2029
|
-
for (const part of parts) {
|
|
2030
|
-
if (isIgnoredDir(part)) return false;
|
|
2031
|
-
}
|
|
2032
|
-
if (isIgnoredFile(path3.basename(filename))) return false;
|
|
2033
|
-
if (isSupported(filename)) return true;
|
|
2034
|
-
if (matchCustomPlugin(path3.resolve(repoPath, filename))) return true;
|
|
2035
|
-
return false;
|
|
2036
|
-
}
|
|
2037
|
-
__name(shouldWatch, "shouldWatch");
|
|
2038
|
-
for (const watchPath of paths) {
|
|
2039
|
-
const resolved = path3.resolve(watchPath);
|
|
2040
|
-
try {
|
|
2041
|
-
const supportsRecursive = process.platform === "darwin" || process.platform === "win32";
|
|
2042
|
-
const watcher = fs2.watch(resolved, { recursive: supportsRecursive }, (_event, filename) => {
|
|
2043
|
-
if (!active || !filename) return;
|
|
2044
|
-
if (!shouldWatch(filename)) return;
|
|
2045
|
-
pending.add(filename);
|
|
2046
|
-
if (timer) clearTimeout(timer);
|
|
2047
|
-
timer = setTimeout(() => processPending(), debounceMs);
|
|
2048
|
-
});
|
|
2049
|
-
watcher.on("error", (err) => {
|
|
2050
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2051
|
-
});
|
|
2052
|
-
watchers.push(watcher);
|
|
2053
|
-
} catch (err) {
|
|
2054
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
return {
|
|
2058
|
-
close() {
|
|
2059
|
-
active = false;
|
|
2060
|
-
if (timer) clearTimeout(timer);
|
|
2061
|
-
for (const w of watchers) w.close();
|
|
2062
|
-
watchers.length = 0;
|
|
2063
|
-
},
|
|
2064
|
-
get active() {
|
|
2065
|
-
return active;
|
|
2066
|
-
}
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
__name(createWatcher, "createWatcher");
|
|
2070
|
-
|
|
2071
|
-
// src/brainbank.ts
|
|
2072
|
-
var BrainBank = class extends EventEmitter {
|
|
2073
|
-
static {
|
|
2074
|
-
__name(this, "BrainBank");
|
|
2075
|
-
}
|
|
2076
|
-
// ── State ───────────────────────────────────────
|
|
2077
|
-
_config;
|
|
2078
|
-
_db;
|
|
2079
|
-
_embedding;
|
|
2080
|
-
_registry = new PluginRegistry();
|
|
2081
|
-
_searchAPI;
|
|
2082
|
-
_indexAPI;
|
|
2083
|
-
_initialized = false;
|
|
2084
|
-
_initPromise = null;
|
|
2085
|
-
_watcher;
|
|
2086
|
-
// Collections (KV store)
|
|
2087
|
-
_collections = /* @__PURE__ */ new Map();
|
|
2088
|
-
_kvHnsw;
|
|
2089
|
-
_kvVecs = /* @__PURE__ */ new Map();
|
|
2090
|
-
// Shared HNSW pool — code:frontend + code:backend share one index
|
|
2091
|
-
_sharedHnsw = /* @__PURE__ */ new Map();
|
|
2092
|
-
constructor(config = {}) {
|
|
2093
|
-
super();
|
|
2094
|
-
this._config = resolveConfig(config);
|
|
2095
|
-
}
|
|
2096
|
-
// ── Plugin registration ──────────────────────────
|
|
2097
|
-
/**
|
|
2098
|
-
* Register a plugin. Chainable.
|
|
2099
|
-
*
|
|
2100
|
-
* brain.use(code({ repoPath: '.' })).use(docs());
|
|
2101
|
-
*/
|
|
2102
|
-
use(plugin) {
|
|
2103
|
-
if (this._initialized)
|
|
2104
|
-
throw new Error(`BrainBank: Cannot add plugin '${plugin.name}' after initialization. Call .use() before any operations.`);
|
|
2105
|
-
this._registry.register(plugin);
|
|
2106
|
-
return this;
|
|
2107
|
-
}
|
|
2108
|
-
/** Get the list of registered plugin names. */
|
|
2109
|
-
get plugins() {
|
|
2110
|
-
return this._registry.names;
|
|
2111
|
-
}
|
|
2112
|
-
/** Check if a plugin is loaded. Also matches type prefix (e.g. 'code' matches 'code:frontend'). */
|
|
2113
|
-
has(name) {
|
|
2114
|
-
return this._registry.has(name);
|
|
2115
|
-
}
|
|
2116
|
-
/** Get a plugin instance. Throws if not loaded. */
|
|
2117
|
-
plugin(n) {
|
|
2118
|
-
return this._registry.get(n);
|
|
2119
|
-
}
|
|
2120
|
-
// ── Initialization ───────────────────────────────
|
|
2121
|
-
/**
|
|
2122
|
-
* Initialize database, HNSW indices, and load existing vectors.
|
|
2123
|
-
* Only initializes registered modules.
|
|
2124
|
-
* Automatically called by index/search methods if not yet initialized.
|
|
2125
|
-
*/
|
|
2126
|
-
async initialize(options = {}) {
|
|
2127
|
-
if (this._initialized) return;
|
|
2128
|
-
if (this._initPromise) return this._initPromise;
|
|
2129
|
-
this._initPromise = this._runInitialize(options).catch((err) => {
|
|
2130
|
-
for (const { hnsw } of this._sharedHnsw.values()) try {
|
|
2131
|
-
hnsw.reinit();
|
|
2132
|
-
} catch {
|
|
2133
|
-
}
|
|
2134
|
-
this._kvVecs.clear();
|
|
2135
|
-
if (this._kvHnsw) try {
|
|
2136
|
-
this._kvHnsw.reinit();
|
|
2137
|
-
} catch {
|
|
2138
|
-
}
|
|
2139
|
-
try {
|
|
2140
|
-
this._db?.close();
|
|
2141
|
-
} catch {
|
|
2142
|
-
}
|
|
2143
|
-
this._db = void 0;
|
|
2144
|
-
this._kvHnsw = void 0;
|
|
2145
|
-
this._searchAPI = void 0;
|
|
2146
|
-
this._indexAPI = void 0;
|
|
2147
|
-
throw err;
|
|
2148
|
-
}).finally(() => {
|
|
2149
|
-
this._initPromise = null;
|
|
2150
|
-
});
|
|
2151
|
-
return this._initPromise;
|
|
2152
|
-
}
|
|
2153
|
-
async _runInitialize(options = {}) {
|
|
2154
|
-
if (this._initialized) return;
|
|
2155
|
-
const initializer = new Initializer(this._config, (e, d) => this.emit(e, d));
|
|
2156
|
-
const early = await initializer.early(options);
|
|
2157
|
-
this._db = early.db;
|
|
2158
|
-
this._embedding = early.embedding;
|
|
2159
|
-
this._kvHnsw = early.kvHnsw;
|
|
2160
|
-
const late = await initializer.late(
|
|
2161
|
-
early,
|
|
2162
|
-
this._registry,
|
|
2163
|
-
this._sharedHnsw,
|
|
2164
|
-
this._kvVecs,
|
|
2165
|
-
(name) => this.collection(name)
|
|
2166
|
-
);
|
|
2167
|
-
this._searchAPI = new SearchAPI({
|
|
2168
|
-
...late,
|
|
2169
|
-
registry: this._registry,
|
|
2170
|
-
config: this._config,
|
|
2171
|
-
getDocsPlugin: /* @__PURE__ */ __name(() => {
|
|
2172
|
-
const docs = this._registry.get("docs");
|
|
2173
|
-
return docs && isCollectionPlugin(docs) ? docs : void 0;
|
|
2174
|
-
}, "getDocsPlugin"),
|
|
2175
|
-
collection: /* @__PURE__ */ __name((n) => this.collection(n), "collection")
|
|
2176
|
-
});
|
|
2177
|
-
this._indexAPI = new IndexAPI({
|
|
2178
|
-
registry: this._registry,
|
|
2179
|
-
gitDepth: this._config.gitDepth,
|
|
2180
|
-
emit: /* @__PURE__ */ __name((e, d) => this.emit(e, d), "emit")
|
|
2181
|
-
});
|
|
2182
|
-
this._initialized = true;
|
|
2183
|
-
this.emit("initialized", { plugins: this.plugins });
|
|
2184
|
-
}
|
|
2185
|
-
// ── Collections (KV) ────────────────────────────
|
|
2186
|
-
/**
|
|
2187
|
-
* Get or create a dynamic collection.
|
|
2188
|
-
* Collections are the universal data primitive — store anything, search semantically.
|
|
2189
|
-
*
|
|
2190
|
-
* const errors = brain.collection('debug_errors');
|
|
2191
|
-
* await errors.add('Fixed null check', { file: 'api.ts' });
|
|
2192
|
-
* const hits = await errors.search('null pointer');
|
|
2193
|
-
*/
|
|
2194
|
-
collection(name) {
|
|
2195
|
-
if (this._collections.has(name)) return this._collections.get(name);
|
|
2196
|
-
if (!this._kvHnsw)
|
|
2197
|
-
throw new Error("BrainBank: Collections not ready. Call await brain.initialize() first.");
|
|
2198
|
-
const coll = new Collection(name, this._db, this._embedding, this._kvHnsw, this._kvVecs, this._config.reranker);
|
|
2199
|
-
this._collections.set(name, coll);
|
|
2200
|
-
return coll;
|
|
2201
|
-
}
|
|
2202
|
-
/** List all collection names that have data. */
|
|
2203
|
-
listCollectionNames() {
|
|
2204
|
-
this._requireInit("listCollectionNames");
|
|
2205
|
-
return this._db.prepare("SELECT DISTINCT collection FROM kv_data ORDER BY collection").all().map((r) => r.collection);
|
|
2206
|
-
}
|
|
2207
|
-
/** Delete a collection's data and evict from cache. */
|
|
2208
|
-
deleteCollection(name) {
|
|
2209
|
-
this._requireInit("deleteCollection");
|
|
2210
|
-
this._db.prepare("DELETE FROM kv_data WHERE collection = ?").run(name);
|
|
2211
|
-
this._collections.delete(name);
|
|
2212
|
-
}
|
|
2213
|
-
// ── Indexing (delegated to IndexAPI) ─────────────
|
|
2214
|
-
async index(options = {}) {
|
|
2215
|
-
await this.initialize();
|
|
2216
|
-
return this._indexAPI.index(options);
|
|
2217
|
-
}
|
|
2218
|
-
/** Index only code files (all repos in multi-repo mode). */
|
|
2219
|
-
async indexCode(options = {}) {
|
|
2220
|
-
await this.initialize();
|
|
2221
|
-
return this._indexAPI.indexCode(options);
|
|
2222
|
-
}
|
|
2223
|
-
/** Index only git history (all repos in multi-repo mode). */
|
|
2224
|
-
async indexGit(options = {}) {
|
|
2225
|
-
await this.initialize();
|
|
2226
|
-
return this._indexAPI.indexGit(options);
|
|
2227
|
-
}
|
|
2228
|
-
// ── Document collections ─────────────────────────
|
|
2229
|
-
/** Register a document collection. */
|
|
2230
|
-
async addCollection(collection) {
|
|
2231
|
-
await this.initialize();
|
|
2232
|
-
this._docsPlugin("addCollection").addCollection(collection);
|
|
2233
|
-
}
|
|
2234
|
-
/** Remove a collection and all its indexed data. */
|
|
2235
|
-
async removeCollection(name) {
|
|
2236
|
-
await this.initialize();
|
|
2237
|
-
this._docsPlugin("removeCollection").removeCollection(name);
|
|
2238
|
-
}
|
|
2239
|
-
/** List all registered collections. */
|
|
2240
|
-
listCollections() {
|
|
2241
|
-
return this._docsPlugin("listCollections").listCollections();
|
|
2242
|
-
}
|
|
2243
|
-
/** Index all (or specific) document collections. */
|
|
2244
|
-
async indexDocs(options = {}) {
|
|
2245
|
-
await this.initialize();
|
|
2246
|
-
const results = await this._docsPlugin("indexDocs").indexCollections(options);
|
|
2247
|
-
this.emit("docsIndexed", results);
|
|
2248
|
-
return results;
|
|
2249
|
-
}
|
|
2250
|
-
/** Search documents only. */
|
|
2251
|
-
async searchDocs(query, options) {
|
|
2252
|
-
await this.initialize();
|
|
2253
|
-
if (!this.has("docs")) return [];
|
|
2254
|
-
return this._docsPlugin("searchDocs").search(query, options);
|
|
2255
|
-
}
|
|
2256
|
-
// ── Context metadata ─────────────────────────────
|
|
2257
|
-
/** Add context description for a collection path. */
|
|
2258
|
-
addContext(collection, path4, context) {
|
|
2259
|
-
const docs = this._docsPlugin("addContext");
|
|
2260
|
-
if (docs.addContext) docs.addContext(collection, path4, context);
|
|
2261
|
-
}
|
|
2262
|
-
/** Remove context for a collection path. */
|
|
2263
|
-
removeContext(collection, path4) {
|
|
2264
|
-
const docs = this._docsPlugin("removeContext");
|
|
2265
|
-
if (docs.removeContext) docs.removeContext(collection, path4);
|
|
2266
|
-
}
|
|
2267
|
-
/** List all context entries. */
|
|
2268
|
-
listContexts() {
|
|
2269
|
-
const docs = this._docsPlugin("listContexts");
|
|
2270
|
-
return docs.listContexts?.() ?? [];
|
|
2271
|
-
}
|
|
2272
|
-
// ── Search (delegated to SearchAPI) ─────────────
|
|
2273
|
-
/**
|
|
2274
|
-
* Get formatted context for a task.
|
|
2275
|
-
* Returns markdown ready for system prompt injection.
|
|
2276
|
-
*/
|
|
2277
|
-
async getContext(task, options = {}) {
|
|
2278
|
-
await this.initialize();
|
|
2279
|
-
return this._searchAPI.getContext(task, options);
|
|
2280
|
-
}
|
|
2281
|
-
/** Semantic search across all loaded modules. */
|
|
2282
|
-
async search(query, options) {
|
|
2283
|
-
await this.initialize();
|
|
2284
|
-
return this._searchAPI.search(query, options);
|
|
2285
|
-
}
|
|
2286
|
-
/** Semantic search over code only. */
|
|
2287
|
-
async searchCode(query, k = 8) {
|
|
2288
|
-
await this.initialize();
|
|
2289
|
-
return this._searchAPI.searchCode(query, k);
|
|
2290
|
-
}
|
|
2291
|
-
/** Semantic search over commits only. */
|
|
2292
|
-
async searchCommits(query, k = 8) {
|
|
2293
|
-
await this.initialize();
|
|
2294
|
-
return this._searchAPI.searchCommits(query, k);
|
|
2295
|
-
}
|
|
2296
|
-
/**
|
|
2297
|
-
* Hybrid search: vector + BM25 fused with Reciprocal Rank Fusion.
|
|
2298
|
-
* Best quality — catches both exact keyword matches and conceptual similarities.
|
|
2299
|
-
*/
|
|
2300
|
-
async hybridSearch(query, options) {
|
|
2301
|
-
await this.initialize();
|
|
2302
|
-
return this._searchAPI.hybridSearch(query, options);
|
|
2303
|
-
}
|
|
2304
|
-
/** BM25 keyword search only (no embeddings needed). */
|
|
2305
|
-
async searchBM25(query, options) {
|
|
2306
|
-
this._requireInit("searchBM25");
|
|
2307
|
-
return this._searchAPI.searchBM25(query, options);
|
|
2308
|
-
}
|
|
2309
|
-
/** Rebuild FTS5 indices. */
|
|
2310
|
-
rebuildFTS() {
|
|
2311
|
-
this._requireInit("rebuildFTS");
|
|
2312
|
-
this._searchAPI.rebuildFTS();
|
|
2313
|
-
}
|
|
2314
|
-
// ── Queries ──────────────────────────────────────
|
|
2315
|
-
/** Get git history for a specific file. */
|
|
2316
|
-
async fileHistory(filePath, limit = 20) {
|
|
2317
|
-
await this.initialize();
|
|
2318
|
-
const gitPlugin = this.plugin("git");
|
|
2319
|
-
return gitPlugin.fileHistory(filePath, limit);
|
|
2320
|
-
}
|
|
2321
|
-
/** Get co-edit suggestions for a file. */
|
|
2322
|
-
coEdits(filePath, limit = 5) {
|
|
2323
|
-
this._requireInit("coEdits");
|
|
2324
|
-
const gitPlugin = this.plugin("git");
|
|
2325
|
-
return gitPlugin.suggestCoEdits(filePath, limit);
|
|
2326
|
-
}
|
|
2327
|
-
// ── Stats ────────────────────────────────────────
|
|
2328
|
-
/** Get statistics for all loaded modules. */
|
|
2329
|
-
stats() {
|
|
2330
|
-
this._requireInit("stats");
|
|
2331
|
-
const result = {};
|
|
2332
|
-
if (this.has("code")) {
|
|
2333
|
-
result.code = this._registry.firstByType("code").stats();
|
|
2334
|
-
}
|
|
2335
|
-
if (this.has("git")) {
|
|
2336
|
-
result.git = this._registry.firstByType("git").stats();
|
|
2337
|
-
}
|
|
2338
|
-
if (this.has("docs")) {
|
|
2339
|
-
result.documents = this._registry.firstByType("docs").stats();
|
|
2340
|
-
}
|
|
2341
|
-
return result;
|
|
2342
|
-
}
|
|
2343
|
-
// ── Watch ────────────────────────────────────────
|
|
2344
|
-
/**
|
|
2345
|
-
* Start watching for file changes and auto-re-index.
|
|
2346
|
-
* Works with built-in and custom indexers.
|
|
2347
|
-
*/
|
|
2348
|
-
watch(options = {}) {
|
|
2349
|
-
this._requireInit("watch");
|
|
2350
|
-
this._watcher?.close();
|
|
2351
|
-
this._watcher = createWatcher(
|
|
2352
|
-
async () => {
|
|
2353
|
-
await this.index();
|
|
2354
|
-
},
|
|
2355
|
-
this._registry.raw,
|
|
2356
|
-
this._config.repoPath,
|
|
2357
|
-
options
|
|
2358
|
-
);
|
|
2359
|
-
return this._watcher;
|
|
2360
|
-
}
|
|
2361
|
-
// ── Re-embed ─────────────────────────────────────
|
|
2362
|
-
/**
|
|
2363
|
-
* Re-embed all existing text with the current embedding provider.
|
|
2364
|
-
* Use this when switching providers (e.g. Local → OpenAI).
|
|
2365
|
-
*/
|
|
2366
|
-
async reembed(options = {}) {
|
|
2367
|
-
this._requireInit("reembed");
|
|
2368
|
-
const hnswMap = /* @__PURE__ */ new Map();
|
|
2369
|
-
if (this._kvHnsw) hnswMap.set("kv", { hnsw: this._kvHnsw, vecs: this._kvVecs });
|
|
2370
|
-
for (const [type, shared] of this._sharedHnsw) {
|
|
2371
|
-
hnswMap.set(type, { hnsw: shared.hnsw, vecs: shared.vecCache });
|
|
2372
|
-
}
|
|
2373
|
-
for (const type of ["memory", "notes", "docs"]) {
|
|
2374
|
-
const mod = this._registry.firstByType(type);
|
|
2375
|
-
if (mod?.hnsw) hnswMap.set(type, { hnsw: mod.hnsw, vecs: mod.vecCache });
|
|
2376
|
-
}
|
|
2377
|
-
const result = await reembedAll(this._db, this._embedding, hnswMap, options);
|
|
2378
|
-
this.emit("reembedded", result);
|
|
2379
|
-
return result;
|
|
2380
|
-
}
|
|
2381
|
-
// ── Lifecycle ────────────────────────────────────
|
|
2382
|
-
/** Close database and release resources. */
|
|
2383
|
-
close() {
|
|
2384
|
-
this._watcher?.close();
|
|
2385
|
-
for (const indexer of this._registry.all) indexer.close?.();
|
|
2386
|
-
this._embedding?.close().catch(() => {
|
|
2387
|
-
});
|
|
2388
|
-
this._db?.close();
|
|
2389
|
-
this._initialized = false;
|
|
2390
|
-
this._collections.clear();
|
|
2391
|
-
this._sharedHnsw.clear();
|
|
2392
|
-
this._kvVecs.clear();
|
|
2393
|
-
this._kvHnsw = void 0;
|
|
2394
|
-
this._searchAPI = void 0;
|
|
2395
|
-
this._indexAPI = void 0;
|
|
2396
|
-
this._registry.clear();
|
|
2397
|
-
}
|
|
2398
|
-
/** Whether the brainbank has been initialized. */
|
|
2399
|
-
get isInitialized() {
|
|
2400
|
-
return this._initialized;
|
|
2401
|
-
}
|
|
2402
|
-
/** The resolved configuration. */
|
|
2403
|
-
get config() {
|
|
2404
|
-
return this._config;
|
|
2405
|
-
}
|
|
2406
|
-
// ── Internal guard ───────────────────────────────
|
|
2407
|
-
_requireInit(method) {
|
|
2408
|
-
if (!this._initialized)
|
|
2409
|
-
throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
|
|
2410
|
-
}
|
|
2411
|
-
/** Get the docs indexer as CollectionPlugin with init + type check. */
|
|
2412
|
-
_docsPlugin(method) {
|
|
2413
|
-
this._requireInit(method);
|
|
2414
|
-
const docs = this._registry.get("docs");
|
|
2415
|
-
if (!docs || !isCollectionPlugin(docs))
|
|
2416
|
-
throw new Error(`BrainBank: Docs indexer not loaded. Add .use(docs()) before calling ${method}().`);
|
|
2417
|
-
return docs;
|
|
2418
|
-
}
|
|
2419
|
-
};
|
|
2420
|
-
|
|
2421
|
-
export {
|
|
2422
|
-
DEFAULTS,
|
|
2423
|
-
resolveConfig,
|
|
2424
|
-
Collection,
|
|
2425
|
-
HNSWIndex,
|
|
2426
|
-
searchMMR,
|
|
2427
|
-
VectorSearch,
|
|
2428
|
-
KeywordSearch,
|
|
2429
|
-
ContextBuilder,
|
|
2430
|
-
BrainBank
|
|
2431
|
-
};
|
|
2432
|
-
//# sourceMappingURL=chunk-DI3H6JVZ.js.map
|