brainbank 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/assets/architecture.png +0 -0
- package/bin/brainbank +18 -0
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-63GBCDS5.js +3249 -0
- package/dist/chunk-63GBCDS5.js.map +1 -0
- package/dist/chunk-DMFMTOHF.js +123 -0
- package/dist/chunk-DMFMTOHF.js.map +1 -0
- package/dist/chunk-FQYKWB2Q.js +136 -0
- package/dist/chunk-FQYKWB2Q.js.map +1 -0
- package/dist/chunk-IMJJ2VEM.js +74 -0
- package/dist/chunk-IMJJ2VEM.js.map +1 -0
- package/dist/chunk-M744PCJQ.js +43 -0
- package/dist/chunk-M744PCJQ.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-OPH7GZ7U.js +124 -0
- package/dist/chunk-OPH7GZ7U.js.map +1 -0
- package/dist/chunk-PXEWQMN7.js +89 -0
- package/dist/chunk-PXEWQMN7.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-VIIHPCC4.js +254 -0
- package/dist/chunk-VIIHPCC4.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3076 -0
- package/dist/cli.js.map +1 -0
- package/dist/haiku-expander-YRSIPGKP.js +8 -0
- package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
- package/dist/haiku-pruner-SHAXUPY6.js +8 -0
- package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
- package/dist/http-server-QUXHLWUM.js +9 -0
- package/dist/http-server-QUXHLWUM.js.map +1 -0
- package/dist/index.d.ts +2161 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/local-embedding-NZQTILGV.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +334 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -0
- package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
- package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
- package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
- package/dist/plugin-IKQ6IRSJ.js +32 -0
- package/dist/plugin-IKQ6IRSJ.js.map +1 -0
- package/dist/resolve-ASGLBNUC.js +10 -0
- package/dist/resolve-ASGLBNUC.js.map +1 -0
- package/dist/stats-tui-ZY2NQSEA.js +1904 -0
- package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
- package/package.json +96 -0
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +179 -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 +77 -0
- package/src/cli/commands/index.ts +482 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp-export.ts +273 -0
- package/src/cli/commands/mcp.ts +6 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +336 -0
- package/src/cli/commands/search.ts +203 -0
- package/src/cli/commands/stats.ts +68 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +47 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +87 -0
- package/src/cli/factory/config-loader.ts +77 -0
- package/src/cli/factory/index.ts +69 -0
- package/src/cli/factory/plugin-loader.ts +325 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/server-client.ts +178 -0
- package/src/cli/tui/index-tui.tsx +667 -0
- package/src/cli/tui/stats-data.ts +523 -0
- package/src/cli/tui/stats-search.ts +262 -0
- package/src/cli/tui/stats-tui.tsx +1465 -0
- package/src/cli/tui/tree-scanner.ts +650 -0
- package/src/cli/utils.ts +137 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +21 -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 +218 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +81 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +218 -0
- package/src/index.ts +154 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +126 -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/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +195 -0
- package/src/mcp/workspace-factory.ts +68 -0
- package/src/mcp/workspace-pool.ts +224 -0
- package/src/plugin.ts +381 -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 +166 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -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 +69 -0
- package/src/search/context-builder.ts +251 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +61 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +384 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +336 -0
- package/src/services/kv-service.ts +64 -0
- package/src/services/plugin-registry.ts +77 -0
- package/src/services/watch.ts +340 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +493 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpServer — Lightweight JSON API for BrainBank.
|
|
3
|
+
*
|
|
4
|
+
* Exposes BrainBank operations over HTTP so CLI commands can delegate
|
|
5
|
+
* to a running server instead of cold-loading models each time.
|
|
6
|
+
*
|
|
7
|
+
* Routes:
|
|
8
|
+
* POST /context → brain.getContext()
|
|
9
|
+
* POST /search → brain.search()
|
|
10
|
+
* POST /hsearch → brain.hybridSearch()
|
|
11
|
+
* POST /ksearch → brain.searchBM25()
|
|
12
|
+
* POST /index → brain.index()
|
|
13
|
+
* GET /health → { ok, pid, uptime, port }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { BrainBank } from '@/brainbank.ts';
|
|
17
|
+
|
|
18
|
+
import * as http from 'node:http';
|
|
19
|
+
|
|
20
|
+
import { DEFAULT_PORT, writePid, removePid } from './daemon.ts';
|
|
21
|
+
|
|
22
|
+
// ── Types ─────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
interface ContextRequest {
|
|
25
|
+
task: string;
|
|
26
|
+
repo?: string;
|
|
27
|
+
sources?: Record<string, number>;
|
|
28
|
+
pathPrefix?: string | string[];
|
|
29
|
+
affectedFiles?: string[];
|
|
30
|
+
codeResults?: number;
|
|
31
|
+
gitResults?: number;
|
|
32
|
+
docsResults?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface IndexRequest {
|
|
36
|
+
repo?: string;
|
|
37
|
+
forceReindex?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SearchRequest {
|
|
41
|
+
query: string;
|
|
42
|
+
repo?: string;
|
|
43
|
+
sources?: Record<string, number>;
|
|
44
|
+
pathPrefix?: string | string[];
|
|
45
|
+
maxResults?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface PoolOptions {
|
|
49
|
+
/** Factory function to create a BrainBank for a repo path. */
|
|
50
|
+
factory: (repoPath: string) => Promise<BrainBank>;
|
|
51
|
+
/** Called when an error occurs. */
|
|
52
|
+
onError?: (repo: string, err: unknown) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Simple Workspace Pool ─────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Minimal in-memory pool for workspace management.
|
|
59
|
+
* Creates BrainBank instances on demand and caches them.
|
|
60
|
+
* Eviction by TTL (30 min inactivity).
|
|
61
|
+
*/
|
|
62
|
+
class SimplePool {
|
|
63
|
+
private _pool = new Map<string, { brain: BrainBank; lastAccess: number }>();
|
|
64
|
+
private _factory: (repoPath: string) => Promise<BrainBank>;
|
|
65
|
+
private _onError?: (repo: string, err: unknown) => void;
|
|
66
|
+
private _timer: ReturnType<typeof setInterval>;
|
|
67
|
+
|
|
68
|
+
private static readonly TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
69
|
+
private static readonly EVICT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
70
|
+
|
|
71
|
+
constructor(options: PoolOptions) {
|
|
72
|
+
this._factory = options.factory;
|
|
73
|
+
this._onError = options.onError;
|
|
74
|
+
this._timer = setInterval(() => this._evictStale(), SimplePool.EVICT_INTERVAL_MS);
|
|
75
|
+
if (this._timer.unref) this._timer.unref();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async get(repoPath: string): Promise<BrainBank> {
|
|
79
|
+
const key = repoPath.replace(/\/+$/, '');
|
|
80
|
+
const existing = this._pool.get(key);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.lastAccess = Date.now();
|
|
83
|
+
try { await existing.brain.ensureFresh(); } catch { /* stale is better than nothing */ }
|
|
84
|
+
return existing.brain;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const brain = await this._factory(key);
|
|
88
|
+
this._pool.set(key, { brain, lastAccess: Date.now() });
|
|
89
|
+
return brain;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
close(): void {
|
|
93
|
+
clearInterval(this._timer);
|
|
94
|
+
for (const [key, entry] of this._pool) {
|
|
95
|
+
try { entry.brain.close(); } catch (err: unknown) {
|
|
96
|
+
this._onError?.(key, err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this._pool.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get size(): number {
|
|
103
|
+
return this._pool.size;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private _evictStale(): void {
|
|
107
|
+
const cutoff = Date.now() - SimplePool.TTL_MS;
|
|
108
|
+
for (const [key, entry] of this._pool) {
|
|
109
|
+
if (entry.lastAccess < cutoff) {
|
|
110
|
+
try { entry.brain.close(); } catch (err: unknown) {
|
|
111
|
+
this._onError?.(key, err);
|
|
112
|
+
}
|
|
113
|
+
this._pool.delete(key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── HTTP Server ───────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export interface HttpServerOptions {
|
|
122
|
+
port?: number;
|
|
123
|
+
/** Factory to create a BrainBank instance for a given repo path. */
|
|
124
|
+
factory: (repoPath: string) => Promise<BrainBank>;
|
|
125
|
+
/** Default repo path when request doesn't specify one. */
|
|
126
|
+
defaultRepo?: string;
|
|
127
|
+
/** Called when an error occurs during pool operations. */
|
|
128
|
+
onError?: (repo: string, err: unknown) => void;
|
|
129
|
+
/** Called on server lifecycle events. */
|
|
130
|
+
onLog?: (msg: string) => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class HttpServer {
|
|
134
|
+
private _server: http.Server | null = null;
|
|
135
|
+
private _pool: SimplePool;
|
|
136
|
+
private _port: number;
|
|
137
|
+
private _defaultRepo: string;
|
|
138
|
+
private _startTime = 0;
|
|
139
|
+
private _log: (msg: string) => void;
|
|
140
|
+
|
|
141
|
+
constructor(options: HttpServerOptions) {
|
|
142
|
+
this._port = options.port ?? DEFAULT_PORT;
|
|
143
|
+
this._defaultRepo = options.defaultRepo ?? process.cwd();
|
|
144
|
+
this._log = options.onLog ?? console.log;
|
|
145
|
+
this._pool = new SimplePool({
|
|
146
|
+
factory: options.factory,
|
|
147
|
+
onError: options.onError,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Start listening. Writes PID file for daemon detection. */
|
|
152
|
+
start(): Promise<void> {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
this._server = http.createServer((req, res) => {
|
|
155
|
+
this._handleRequest(req, res).catch((err: unknown) => {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
this._log(`Request error: ${msg}`);
|
|
158
|
+
if (!res.headersSent) {
|
|
159
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
160
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this._server.on('error', (err: NodeJS.ErrnoException) => {
|
|
166
|
+
if (err.code === 'EADDRINUSE') {
|
|
167
|
+
reject(new Error(`Port ${this._port} already in use. Is another server running?`));
|
|
168
|
+
} else {
|
|
169
|
+
reject(err);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this._server.listen(this._port, '127.0.0.1', () => {
|
|
174
|
+
this._startTime = Date.now();
|
|
175
|
+
writePid(process.pid, this._port);
|
|
176
|
+
this._log(`BrainBank HTTP server listening on http://localhost:${this._port}`);
|
|
177
|
+
resolve();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Stop the server and clean up. */
|
|
183
|
+
close(): void {
|
|
184
|
+
this._pool.close();
|
|
185
|
+
this._server?.close();
|
|
186
|
+
this._server = null;
|
|
187
|
+
removePid();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get port(): number {
|
|
191
|
+
return this._port;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Request Router ──────────────────────────────────
|
|
195
|
+
|
|
196
|
+
private async _handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
197
|
+
// CORS + content type
|
|
198
|
+
res.setHeader('Content-Type', 'application/json');
|
|
199
|
+
|
|
200
|
+
// Health check — no body parsing needed
|
|
201
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
202
|
+
return this._handleHealth(res);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (req.method !== 'POST') {
|
|
206
|
+
res.writeHead(405);
|
|
207
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const body = await this._readBody(req);
|
|
212
|
+
|
|
213
|
+
switch (req.url) {
|
|
214
|
+
case '/context':
|
|
215
|
+
return this._handleContext(body, res);
|
|
216
|
+
case '/search':
|
|
217
|
+
return this._handleSearch(body, res, 'search');
|
|
218
|
+
case '/hsearch':
|
|
219
|
+
return this._handleSearch(body, res, 'hybrid');
|
|
220
|
+
case '/ksearch':
|
|
221
|
+
return this._handleSearch(body, res, 'keyword');
|
|
222
|
+
case '/index':
|
|
223
|
+
return this._handleIndex(body, res);
|
|
224
|
+
default:
|
|
225
|
+
res.writeHead(404);
|
|
226
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Handlers ────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
private _handleHealth(res: http.ServerResponse): void {
|
|
233
|
+
const uptime = Math.round((Date.now() - this._startTime) / 1000);
|
|
234
|
+
res.writeHead(200);
|
|
235
|
+
res.end(JSON.stringify({
|
|
236
|
+
ok: true,
|
|
237
|
+
pid: process.pid,
|
|
238
|
+
port: this._port,
|
|
239
|
+
uptime,
|
|
240
|
+
workspaces: this._pool.size,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async _handleContext(body: unknown, res: http.ServerResponse): Promise<void> {
|
|
245
|
+
const req = body as ContextRequest;
|
|
246
|
+
if (!req.task) {
|
|
247
|
+
res.writeHead(400);
|
|
248
|
+
res.end(JSON.stringify({ error: 'Missing required field: task' }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const repoPath = req.repo ?? this._defaultRepo;
|
|
253
|
+
const brain = await this._pool.get(repoPath);
|
|
254
|
+
|
|
255
|
+
// Build sources from explicit params, then let `sources` override
|
|
256
|
+
const base: Record<string, number> = {
|
|
257
|
+
code: req.codeResults ?? 20,
|
|
258
|
+
git: req.gitResults ?? 5,
|
|
259
|
+
};
|
|
260
|
+
if (req.docsResults !== undefined) base.docs = req.docsResults;
|
|
261
|
+
const resolvedSources = req.sources ? { ...base, ...req.sources } : base;
|
|
262
|
+
|
|
263
|
+
const context = await brain.getContext(req.task, {
|
|
264
|
+
affectedFiles: req.affectedFiles,
|
|
265
|
+
sources: resolvedSources,
|
|
266
|
+
pathPrefix: req.pathPrefix,
|
|
267
|
+
source: 'daemon',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
res.writeHead(200);
|
|
271
|
+
res.end(JSON.stringify({ context }));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async _handleIndex(body: unknown, res: http.ServerResponse): Promise<void> {
|
|
275
|
+
const req = body as IndexRequest;
|
|
276
|
+
const repoPath = req.repo ?? this._defaultRepo;
|
|
277
|
+
const brain = await this._pool.get(repoPath);
|
|
278
|
+
|
|
279
|
+
const result = await brain.index({ forceReindex: req.forceReindex });
|
|
280
|
+
|
|
281
|
+
res.writeHead(200);
|
|
282
|
+
res.end(JSON.stringify({ result }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async _handleSearch(
|
|
286
|
+
body: unknown, res: http.ServerResponse,
|
|
287
|
+
mode: 'search' | 'hybrid' | 'keyword',
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
const req = body as SearchRequest;
|
|
290
|
+
if (!req.query) {
|
|
291
|
+
res.writeHead(400);
|
|
292
|
+
res.end(JSON.stringify({ error: 'Missing required field: query' }));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const repoPath = req.repo ?? this._defaultRepo;
|
|
297
|
+
const brain = await this._pool.get(repoPath);
|
|
298
|
+
|
|
299
|
+
const opts = {
|
|
300
|
+
sources: req.sources,
|
|
301
|
+
pathPrefix: req.pathPrefix,
|
|
302
|
+
source: 'daemon' as const,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
let results;
|
|
306
|
+
switch (mode) {
|
|
307
|
+
case 'search': results = await brain.search(req.query, opts); break;
|
|
308
|
+
case 'hybrid': results = await brain.hybridSearch(req.query, opts); break;
|
|
309
|
+
case 'keyword': results = await brain.searchBM25(req.query, opts); break;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const maxResults = req.maxResults ?? 20;
|
|
313
|
+
results = results.slice(0, maxResults);
|
|
314
|
+
|
|
315
|
+
res.writeHead(200);
|
|
316
|
+
res.end(JSON.stringify({ results }));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Helpers ─────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
private _readBody(req: http.IncomingMessage): Promise<unknown> {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const chunks: Buffer[] = [];
|
|
324
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
325
|
+
req.on('error', reject);
|
|
326
|
+
req.on('end', () => {
|
|
327
|
+
try {
|
|
328
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
329
|
+
resolve(raw ? JSON.parse(raw) as unknown : {});
|
|
330
|
+
} catch {
|
|
331
|
+
resolve({});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — KV Service
|
|
3
|
+
*
|
|
4
|
+
* Owns the shared HNSW index and vector cache for KV collections.
|
|
5
|
+
* Provides collection creation, listing, and deletion.
|
|
6
|
+
* Extracted from BrainBank to separate infrastructure from facade.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DatabaseAdapter } from '@/db/adapter.ts';
|
|
10
|
+
import type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';
|
|
11
|
+
import type { EmbeddingProvider } from '@/types.ts';
|
|
12
|
+
import { Collection } from './collection.ts';
|
|
13
|
+
|
|
14
|
+
export class KVService {
|
|
15
|
+
private _collections = new Map<string, Collection>();
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private _db: DatabaseAdapter,
|
|
19
|
+
private _embedding: EmbeddingProvider,
|
|
20
|
+
private _hnsw: HNSWIndex,
|
|
21
|
+
private _vecs: Map<number, Float32Array>,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/** Get or create a named collection. */
|
|
25
|
+
collection(name: string): Collection {
|
|
26
|
+
if (this._collections.has(name)) return this._collections.get(name)!;
|
|
27
|
+
const coll = new Collection(name, this._db, this._embedding, this._hnsw, this._vecs);
|
|
28
|
+
this._collections.set(name, coll);
|
|
29
|
+
return coll;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** List all collection names that have data. */
|
|
33
|
+
listNames(): string[] {
|
|
34
|
+
return (this._db.prepare('SELECT DISTINCT collection FROM kv_data ORDER BY collection').all() as { collection: string }[])
|
|
35
|
+
.map(r => r.collection);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Delete a collection's data and evict from cache. Removes vectors from HNSW to prevent ghost entries. */
|
|
39
|
+
delete(name: string): void {
|
|
40
|
+
const ids = this._db.prepare(
|
|
41
|
+
'SELECT id FROM kv_data WHERE collection = ?'
|
|
42
|
+
).all(name) as { id: number }[];
|
|
43
|
+
|
|
44
|
+
for (const { id } of ids) {
|
|
45
|
+
this._hnsw.remove(id);
|
|
46
|
+
this._vecs.delete(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._db.prepare('DELETE FROM kv_data WHERE collection = ?').run(name);
|
|
50
|
+
this._collections.delete(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Access the shared HNSW index (used by reembed). */
|
|
54
|
+
get hnsw(): HNSWIndex { return this._hnsw; }
|
|
55
|
+
|
|
56
|
+
/** Access the shared vector cache. @internal */
|
|
57
|
+
get vecs(): Map<number, Float32Array> { return this._vecs; }
|
|
58
|
+
|
|
59
|
+
/** Clear all cached collections and vectors. */
|
|
60
|
+
clear(): void {
|
|
61
|
+
this._collections.clear();
|
|
62
|
+
this._vecs.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages registration and lookup of plugins.
|
|
5
|
+
* Extracted from BrainBank so the facade stays focused on orchestration.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Store plugins by name
|
|
9
|
+
* - Alias resolution (currently none; add here if needed)
|
|
10
|
+
* - Consistent error messages on missing plugins
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Plugin } from '@/plugin.ts';
|
|
14
|
+
|
|
15
|
+
/** Shorthand aliases that map public names to canonical plugin names. */
|
|
16
|
+
const ALIASES: Readonly<Record<string, string>> = {
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class PluginRegistry {
|
|
20
|
+
private _map = new Map<string, Plugin>();
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/** Store a plugin. Duplicate names silently overwrite. */
|
|
24
|
+
register(plugin: Plugin): void {
|
|
25
|
+
this._map.set(plugin.name, plugin);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/** Check whether a plugin is registered (exact match). */
|
|
30
|
+
has(name: string): boolean {
|
|
31
|
+
return this._map.has(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a plugin by name. Throws a descriptive error if not found.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order:
|
|
38
|
+
* 1. Alias map (currently empty)
|
|
39
|
+
* 2. Exact match
|
|
40
|
+
*/
|
|
41
|
+
get<T extends Plugin = Plugin>(name: string): T {
|
|
42
|
+
const resolved = ALIASES[name] ?? name;
|
|
43
|
+
|
|
44
|
+
const exact = this._map.get(resolved);
|
|
45
|
+
if (exact) return exact as T;
|
|
46
|
+
|
|
47
|
+
throw new Error(
|
|
48
|
+
`BrainBank: Plugin '${name}' is not loaded. ` +
|
|
49
|
+
`Add .use(${name}()) to your BrainBank instance.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/** All registered plugin names (insertion order). */
|
|
55
|
+
get names(): string[] {
|
|
56
|
+
return [...this._map.keys()];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** All registered plugin instances (insertion order). */
|
|
60
|
+
get all(): Plugin[] {
|
|
61
|
+
return [...this._map.values()];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Underlying Map.
|
|
66
|
+
* Prefer `all` everywhere else.
|
|
67
|
+
*/
|
|
68
|
+
get raw(): Map<string, Plugin> {
|
|
69
|
+
return this._map;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
/** Remove all registered plugins. Called by BrainBank.close(). */
|
|
74
|
+
clear(): void {
|
|
75
|
+
this._map.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|