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
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Plugin System
|
|
3
|
+
*
|
|
4
|
+
* Plugins are pluggable strategies that scan external data sources
|
|
5
|
+
* and push content into BrainBank. Built-in plugins handle code,
|
|
6
|
+
* git, and docs. Third-party frameworks (LangChain, etc.)
|
|
7
|
+
* can implement custom plugins.
|
|
8
|
+
*
|
|
9
|
+
* import { BrainBank } from 'brainbank';
|
|
10
|
+
* import { code } from 'brainbank/indexers/code';
|
|
11
|
+
*
|
|
12
|
+
* const brain = new BrainBank()
|
|
13
|
+
* .use(code({ repoPath: '.' }));
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DatabaseAdapter } from './db/adapter.ts';
|
|
17
|
+
import type { Migration } from './db/migrations.ts';
|
|
18
|
+
import type { IncrementalTracker } from './db/tracker.ts';
|
|
19
|
+
import type { HNSWIndex } from './providers/vector/hnsw-index.ts';
|
|
20
|
+
import type { DomainVectorSearch } from './search/types.ts';
|
|
21
|
+
import type { WebhookServer } from './services/webhook-server.ts';
|
|
22
|
+
import type {
|
|
23
|
+
EmbeddingProvider, SearchResult, IndexResult, ProgressCallback,
|
|
24
|
+
ResolvedConfig, DocumentCollection, ICollection,
|
|
25
|
+
WatchEventHandler, WatchHandle, WatchConfig,
|
|
26
|
+
ExpanderManifestItem,
|
|
27
|
+
} from './types.ts';
|
|
28
|
+
|
|
29
|
+
// Provided to each plugin during initialization.
|
|
30
|
+
|
|
31
|
+
export interface PluginContext {
|
|
32
|
+
/** Database adapter (shared across all plugins). */
|
|
33
|
+
db: DatabaseAdapter;
|
|
34
|
+
/** Embedding provider (shared). */
|
|
35
|
+
embedding: EmbeddingProvider;
|
|
36
|
+
/** Resolved BrainBank config. */
|
|
37
|
+
config: ResolvedConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Create and initialize an HNSW index.
|
|
40
|
+
* Pass `name` to enable disk persistence (recommended).
|
|
41
|
+
*
|
|
42
|
+
* **Private vs shared:** Use `getOrCreateSharedHnsw()` for indexes that should be
|
|
43
|
+
* part of the composite search (code, git) and persisted across restarts.
|
|
44
|
+
* Use `createHnsw()` for plugin-local indexes that don't participate in the
|
|
45
|
+
* main search pipeline (e.g. internal similarity lookups).
|
|
46
|
+
*/
|
|
47
|
+
createHnsw(maxElements?: number, dims?: number, name?: string): Promise<HNSWIndex>;
|
|
48
|
+
/** Load existing vectors from a SQLite vectors table into an HNSW index + cache. */
|
|
49
|
+
loadVectors(table: string, idCol: string, hnsw: HNSWIndex, cache: Map<number, Float32Array>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get or create a shared HNSW index by key.
|
|
52
|
+
*
|
|
53
|
+
* **HNSW sharing strategies:**
|
|
54
|
+
* The `type` key determines sharing behavior. Two plugins that pass the
|
|
55
|
+
* same key share one HNSW index; different keys get separate indexes.
|
|
56
|
+
*
|
|
57
|
+
* | Plugin type | Key passed | Sharing behavior |
|
|
58
|
+
* |------------|------------------|------------------------------------------|
|
|
59
|
+
* | git | `'git'` | All `git:*` repos share one HNSW |
|
|
60
|
+
* | docs | `'docs'` | All docs share one HNSW |
|
|
61
|
+
* | code | `this.name` | Each `code:*` repo gets its own HNSW |
|
|
62
|
+
*
|
|
63
|
+
* **Rule of thumb:**
|
|
64
|
+
* - Same key = shared index (saves memory, single search covers all)
|
|
65
|
+
* - Plugin name as key = per-repo index (avoids cross-repo noise)
|
|
66
|
+
*
|
|
67
|
+
* The key is also used for hot-reload (`ensureFresh`) and disk persistence
|
|
68
|
+
* (`hnsw-<key>.index`), so it must match the key used in `bumpVersion()`.
|
|
69
|
+
*/
|
|
70
|
+
getOrCreateSharedHnsw(type: string, maxElements?: number, dims?: number): Promise<{ hnsw: HNSWIndex; vecCache: Map<number, Float32Array>; isNew: boolean }>;
|
|
71
|
+
/** Get or create a dynamic collection. */
|
|
72
|
+
collection(name: string): ICollection;
|
|
73
|
+
/**
|
|
74
|
+
* Create an incremental tracker scoped to this plugin.
|
|
75
|
+
* Provides `isUnchanged`, `markIndexed`, `findOrphans`, `remove`, `clear`
|
|
76
|
+
* for standardized add/update/delete detection during indexing.
|
|
77
|
+
*/
|
|
78
|
+
createTracker(): IncrementalTracker;
|
|
79
|
+
/** Optional webhook server for push-based watch plugins. undefined if not configured. */
|
|
80
|
+
webhookServer?: WebhookServer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Minimal contract: name + initialize. All capabilities are expressed
|
|
84
|
+
// via composed interfaces below.
|
|
85
|
+
|
|
86
|
+
export interface Plugin {
|
|
87
|
+
/** Unique plugin name (e.g. 'code', 'git', 'docs'). */
|
|
88
|
+
readonly name: string;
|
|
89
|
+
/** Initialize the plugin (create HNSW, load vectors, etc.). */
|
|
90
|
+
initialize(ctx: PluginContext): Promise<void>;
|
|
91
|
+
/** Return stats for this plugin. */
|
|
92
|
+
stats?(): Record<string, number | string>;
|
|
93
|
+
/** Clean up resources. */
|
|
94
|
+
close?(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Implemented by plugins that support specific capabilities.
|
|
98
|
+
// Use type guards below to check at runtime.
|
|
99
|
+
|
|
100
|
+
/** Options accepted by IndexablePlugin.index(). */
|
|
101
|
+
export interface IndexOptions {
|
|
102
|
+
forceReindex?: boolean;
|
|
103
|
+
depth?: number;
|
|
104
|
+
onProgress?: ProgressCallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Plugins that can scan and index content (code, git). */
|
|
108
|
+
export interface IndexablePlugin extends Plugin {
|
|
109
|
+
index(options?: IndexOptions): Promise<IndexResult>;
|
|
110
|
+
/** Incremental: re-index only specific items by ID. Falls back to index() if not implemented. */
|
|
111
|
+
indexItems?(ids: string[]): Promise<IndexResult>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Plugins that can search indexed content (docs). */
|
|
115
|
+
export interface SearchablePlugin extends Plugin {
|
|
116
|
+
search(query: string, options?: Record<string, unknown>): Promise<SearchResult[]>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Plugins that can watch their own data source for changes. */
|
|
120
|
+
export interface WatchablePlugin extends Plugin {
|
|
121
|
+
/** Start watching. Plugin controls how (fs.watch, polling, webhook, etc.). */
|
|
122
|
+
watch(onEvent: WatchEventHandler): WatchHandle;
|
|
123
|
+
/** Optional hints for the core (debounce, batching, priority). */
|
|
124
|
+
watchConfig?(): WatchConfig;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/** Check if a plugin can scan/index content. */
|
|
129
|
+
export function isIndexable(i: Plugin): i is IndexablePlugin {
|
|
130
|
+
return typeof (i as IndexablePlugin).index === 'function';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Check if a plugin can search content. */
|
|
134
|
+
export function isSearchable(i: Plugin): i is SearchablePlugin {
|
|
135
|
+
return typeof (i as SearchablePlugin).search === 'function';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Check if a plugin can watch its own data source. */
|
|
139
|
+
export function isWatchable(i: Plugin): i is WatchablePlugin {
|
|
140
|
+
return typeof (i as WatchablePlugin).watch === 'function';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Path-specific context metadata for document collections. */
|
|
144
|
+
export interface PathContext {
|
|
145
|
+
collection: string;
|
|
146
|
+
path: string;
|
|
147
|
+
context: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Plugins that manage document collections (docs). */
|
|
151
|
+
export interface DocsPlugin extends SearchablePlugin {
|
|
152
|
+
addCollection(collection: DocumentCollection): void;
|
|
153
|
+
removeCollection(name: string): void;
|
|
154
|
+
listCollections(): DocumentCollection[];
|
|
155
|
+
indexDocs(options?: { onProgress?: (collection: string, file: string, current: number, total: number) => void }): Promise<Record<string, { indexed: number; skipped: number; removed: number; chunks: number }>>;
|
|
156
|
+
addContext(collection: string, path: string, context: string): void;
|
|
157
|
+
listContexts(): PathContext[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Check if a plugin manages document collections. */
|
|
161
|
+
export function isDocsPlugin(i: Plugin): i is DocsPlugin {
|
|
162
|
+
return typeof (i as DocsPlugin).addCollection === 'function'
|
|
163
|
+
&& typeof (i as DocsPlugin).listCollections === 'function';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/** Plugin that provides co-edit suggestions (e.g. git). */
|
|
168
|
+
export interface CoEditPlugin extends Plugin {
|
|
169
|
+
coEdits: {
|
|
170
|
+
suggest(filePath: string, limit: number): { file: string; count: number }[];
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Check if a plugin provides co-edit suggestions. */
|
|
175
|
+
export function isCoEditPlugin(p: Plugin): p is CoEditPlugin {
|
|
176
|
+
return 'coEdits' in p && typeof (p as CoEditPlugin).coEdits?.suggest === 'function';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/** Table descriptor for re-embedding — maps text rows to vector BLOBs. */
|
|
181
|
+
export interface ReembedTable {
|
|
182
|
+
/** Human-readable name (for progress). */
|
|
183
|
+
name: string;
|
|
184
|
+
/** Table with text content. */
|
|
185
|
+
textTable: string;
|
|
186
|
+
/** Table with vector BLOBs. */
|
|
187
|
+
vectorTable: string;
|
|
188
|
+
/** PK column in text table. */
|
|
189
|
+
idColumn: string;
|
|
190
|
+
/** FK column in vector table. */
|
|
191
|
+
fkColumn: string;
|
|
192
|
+
/** Build the embedding text from a DB row. */
|
|
193
|
+
textBuilder: (row: Record<string, unknown>) => string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Plugins that own vector tables and can rebuild embedding text from DB rows. */
|
|
197
|
+
export interface ReembeddablePlugin extends Plugin {
|
|
198
|
+
/** Table descriptor for re-embedding. */
|
|
199
|
+
reembedConfig(): ReembedTable;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Check if a plugin supports re-embedding. */
|
|
203
|
+
export function isReembeddable(p: Plugin): p is ReembeddablePlugin {
|
|
204
|
+
return typeof (p as ReembeddablePlugin).reembedConfig === 'function';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/** Plugin that provides a domain-specific vector search strategy. */
|
|
209
|
+
export interface VectorSearchPlugin extends Plugin {
|
|
210
|
+
/** Create the domain vector search (called during SearchAPI wiring). */
|
|
211
|
+
createVectorSearch(): DomainVectorSearch | undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Check if a plugin provides a domain vector search. */
|
|
215
|
+
export function isVectorSearchPlugin(p: Plugin): p is VectorSearchPlugin {
|
|
216
|
+
return typeof (p as VectorSearchPlugin).createVectorSearch === 'function';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Describes a configurable context field that a plugin supports. */
|
|
220
|
+
export interface ContextFieldDef {
|
|
221
|
+
/** Field name (e.g. 'lines', 'callTree', 'symbols'). Must be unique per plugin. */
|
|
222
|
+
name: string;
|
|
223
|
+
/** Accepted value type. 'object' allows nested config like `{ depth: 3 }`. */
|
|
224
|
+
type: 'boolean' | 'number' | 'object';
|
|
225
|
+
/** Default value (used when not specified in config or query). */
|
|
226
|
+
default: unknown;
|
|
227
|
+
/** Human-readable description for CLI --help and MCP tool descriptions. */
|
|
228
|
+
description: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Plugin that declares configurable context fields. */
|
|
232
|
+
export interface ContextFieldPlugin extends Plugin {
|
|
233
|
+
/** Declare available context fields. Called once during setup. */
|
|
234
|
+
contextFields(): ContextFieldDef[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Check if a plugin declares context fields. */
|
|
238
|
+
export function isContextFieldPlugin(p: Plugin): p is ContextFieldPlugin {
|
|
239
|
+
return typeof (p as ContextFieldPlugin).contextFields === 'function';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Plugin that contributes sections to the context builder output. */
|
|
243
|
+
export interface ContextFormatterPlugin extends Plugin {
|
|
244
|
+
/**
|
|
245
|
+
* Append formatted markdown sections to `parts`.
|
|
246
|
+
* `fields` contains resolved context fields (plugin defaults ← config ← per-query).
|
|
247
|
+
*/
|
|
248
|
+
formatContext(results: SearchResult[], parts: string[], fields: Record<string, unknown>): void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Check if a plugin provides context formatting. */
|
|
252
|
+
export function isContextFormatterPlugin(p: Plugin): p is ContextFormatterPlugin {
|
|
253
|
+
return typeof (p as ContextFormatterPlugin).formatContext === 'function';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
/** Plugin that owns database tables and supports versioned migrations. */
|
|
258
|
+
export interface MigratablePlugin extends Plugin {
|
|
259
|
+
/** Current schema version for this plugin. */
|
|
260
|
+
readonly schemaVersion: number;
|
|
261
|
+
/** Ordered list of migrations (version 1, 2, 3, …). */
|
|
262
|
+
readonly migrations: Migration[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Check if a plugin supports schema migrations. */
|
|
266
|
+
export function isMigratable(p: Plugin): p is MigratablePlugin {
|
|
267
|
+
return typeof (p as MigratablePlugin).schemaVersion === 'number'
|
|
268
|
+
&& Array.isArray((p as MigratablePlugin).migrations);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/** Plugin that can do FTS5 keyword search on its own tables. */
|
|
273
|
+
export interface BM25SearchPlugin extends Plugin {
|
|
274
|
+
/** Run BM25 keyword search. Returns scored results. */
|
|
275
|
+
searchBM25(query: string, k: number, minScore?: number): SearchResult[];
|
|
276
|
+
/** Rebuild the FTS5 index from the content table. */
|
|
277
|
+
rebuildFTS?(): void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Check if a plugin provides BM25 keyword search. */
|
|
281
|
+
export function isBM25SearchPlugin(p: Plugin): p is BM25SearchPlugin {
|
|
282
|
+
return typeof (p as BM25SearchPlugin).searchBM25 === 'function';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Plugin that supports context expansion (provides manifest + resolves chunk IDs). */
|
|
286
|
+
export interface ExpandablePlugin extends Plugin {
|
|
287
|
+
/**
|
|
288
|
+
* Build a manifest of candidate chunks for LLM expansion.
|
|
289
|
+
* Returns chunks from files NOT already in search results.
|
|
290
|
+
* Priority chunks (from import graph neighbors) are marked with `priority: true`.
|
|
291
|
+
*
|
|
292
|
+
* @param excludeFilePaths File paths already present in search results — excluded from manifest.
|
|
293
|
+
* @param excludeIds Chunk IDs already in search results — excluded from manifest.
|
|
294
|
+
* @param resultFilePaths File paths in search results — used to query import graph for priority chunks.
|
|
295
|
+
*/
|
|
296
|
+
buildManifest(excludeFilePaths: string[], excludeIds: number[], resultFilePaths?: string[]): ExpanderManifestItem[];
|
|
297
|
+
/**
|
|
298
|
+
* Resolve chunk IDs back into SearchResults.
|
|
299
|
+
* Called after the expander selects additional IDs.
|
|
300
|
+
*/
|
|
301
|
+
resolveChunks(ids: number[]): SearchResult[];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Check if a plugin supports context expansion. */
|
|
305
|
+
export function isExpandablePlugin(p: Plugin): p is ExpandablePlugin {
|
|
306
|
+
return typeof (p as ExpandablePlugin).buildManifest === 'function'
|
|
307
|
+
&& typeof (p as ExpandablePlugin).resolveChunks === 'function';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
/** Plugin that can resolve file paths/patterns directly to SearchResults (no search). */
|
|
312
|
+
export interface FileResolvablePlugin extends Plugin {
|
|
313
|
+
/**
|
|
314
|
+
* Resolve file paths, directories, and glob patterns to SearchResults.
|
|
315
|
+
* Each entry is resolved: exact → directory → glob → fuzzy basename fallback.
|
|
316
|
+
*
|
|
317
|
+
* @param patterns - File paths, directory prefixes (trailing `/`), or glob patterns (`*`).
|
|
318
|
+
*/
|
|
319
|
+
resolveFiles(patterns: string[]): SearchResult[];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Check if a plugin can resolve files directly. */
|
|
323
|
+
export function isFileResolvable(p: Plugin): p is FileResolvablePlugin {
|
|
324
|
+
return typeof (p as FileResolvablePlugin).resolveFiles === 'function';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// ── Third-party Plugin Discovery (TUI integration) ────────────────
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Scan info for the TUI sidebar. Describes what content a plugin can index.
|
|
332
|
+
* Exported standalone by plugin packages — called BEFORE plugin initialization.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* // brainbank-csv/index.ts
|
|
337
|
+
* export function scan(repoPath: string): PluginScanInfo { ... }
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
export interface PluginScanInfo {
|
|
341
|
+
/** Plugin name (e.g. 'csv', 'openapi'). */
|
|
342
|
+
name: string;
|
|
343
|
+
/** Whether there's content available to index. */
|
|
344
|
+
available: boolean;
|
|
345
|
+
/** Human-readable summary (e.g. '12 CSV files'). */
|
|
346
|
+
summary: string;
|
|
347
|
+
/** Emoji icon for TUI display. */
|
|
348
|
+
icon: string;
|
|
349
|
+
/** Whether checked by default in the module selector. */
|
|
350
|
+
checked: boolean;
|
|
351
|
+
/** Reason this plugin is disabled (shown when unavailable). */
|
|
352
|
+
disabled?: string;
|
|
353
|
+
/** Detail lines for the scan tree (e.g. per-file breakdown). */
|
|
354
|
+
details?: string[];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* A single line in the TUI explorer preview panel.
|
|
359
|
+
* Returned by `preview()` — rendered as-is in the right panel.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* // brainbank-csv/index.ts
|
|
364
|
+
* export function preview(repoPath: string): PluginPreviewLine[] {
|
|
365
|
+
* return [
|
|
366
|
+
* { text: '📊 3 CSV files', bold: true },
|
|
367
|
+
* { text: ' sales.csv 2.3 MB', color: '#9ECE6A' },
|
|
368
|
+
* ];
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
export interface PluginPreviewLine {
|
|
373
|
+
/** Text content for this line. */
|
|
374
|
+
text: string;
|
|
375
|
+
/** Optional hex color (e.g. '#9ECE6A'). */
|
|
376
|
+
color?: string;
|
|
377
|
+
/** Render bold. */
|
|
378
|
+
bold?: boolean;
|
|
379
|
+
/** Render dimmed. */
|
|
380
|
+
dim?: boolean;
|
|
381
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Embedding Worker Thread
|
|
3
|
+
*
|
|
4
|
+
* Worker script that runs the embedding provider in a dedicated thread.
|
|
5
|
+
* Receives text from the main thread, returns Float32Array vectors.
|
|
6
|
+
* Keeps the main thread's event loop free for serving search requests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
10
|
+
|
|
11
|
+
/** Message types for main ↔ worker communication. */
|
|
12
|
+
interface EmbedRequest {
|
|
13
|
+
id: number;
|
|
14
|
+
type: 'embed' | 'embedBatch' | 'close';
|
|
15
|
+
text?: string;
|
|
16
|
+
texts?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface EmbedResponse {
|
|
20
|
+
id: number;
|
|
21
|
+
type: 'result' | 'error';
|
|
22
|
+
vector?: ArrayBuffer;
|
|
23
|
+
vectors?: ArrayBuffer[];
|
|
24
|
+
dims?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Minimal interface for the provider loaded in the worker. */
|
|
29
|
+
interface WorkerProvider {
|
|
30
|
+
dims: number;
|
|
31
|
+
embed(text: string): Promise<Float32Array>;
|
|
32
|
+
embedBatch(texts: string[]): Promise<Float32Array[]>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main(): Promise<void> {
|
|
37
|
+
if (!parentPort) throw new Error('Must run as worker thread');
|
|
38
|
+
|
|
39
|
+
const config = workerData as { providerType: string; providerOptions: Record<string, unknown> };
|
|
40
|
+
let provider: WorkerProvider;
|
|
41
|
+
|
|
42
|
+
// Dynamically instantiate the provider based on type
|
|
43
|
+
if (config.providerType === 'local') {
|
|
44
|
+
const { LocalEmbedding } = await import('./local-embedding.ts') as { LocalEmbedding: new (opts?: Record<string, unknown>) => WorkerProvider };
|
|
45
|
+
provider = new LocalEmbedding(config.providerOptions);
|
|
46
|
+
} else if (config.providerType === 'openai') {
|
|
47
|
+
const { OpenAIEmbedding } = await import('./openai-embedding.ts') as { OpenAIEmbedding: new (opts?: Record<string, unknown>) => WorkerProvider };
|
|
48
|
+
provider = new OpenAIEmbedding(config.providerOptions);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`BrainBank: Unknown embedding provider type '${config.providerType}' in worker.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
parentPort.on('message', async (msg: EmbedRequest) => {
|
|
54
|
+
if (msg.type === 'close') {
|
|
55
|
+
await provider.close();
|
|
56
|
+
parentPort!.postMessage({ id: msg.id, type: 'result' } satisfies EmbedResponse);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (msg.type === 'embed' && msg.text) {
|
|
62
|
+
const vec = await provider.embed(msg.text);
|
|
63
|
+
const buffer = vec.buffer as ArrayBuffer;
|
|
64
|
+
parentPort!.postMessage(
|
|
65
|
+
{ id: msg.id, type: 'result', vector: buffer, dims: provider.dims } satisfies EmbedResponse,
|
|
66
|
+
[buffer],
|
|
67
|
+
);
|
|
68
|
+
} else if (msg.type === 'embedBatch' && msg.texts) {
|
|
69
|
+
const vecs = await provider.embedBatch(msg.texts);
|
|
70
|
+
const buffers = vecs.map(v => v.buffer as ArrayBuffer);
|
|
71
|
+
parentPort!.postMessage(
|
|
72
|
+
{
|
|
73
|
+
id: msg.id,
|
|
74
|
+
type: 'result',
|
|
75
|
+
vectors: buffers,
|
|
76
|
+
dims: provider.dims,
|
|
77
|
+
} satisfies EmbedResponse,
|
|
78
|
+
buffers,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
parentPort!.postMessage({ id: msg.id, type: 'error', error: message } satisfies EmbedResponse);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Signal ready
|
|
88
|
+
parentPort.postMessage({ id: 0, type: 'result', dims: provider.dims } satisfies EmbedResponse);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch(err => {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
parentPort?.postMessage({ id: -1, type: 'error', error: message });
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Embedding Worker Proxy
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for any EmbeddingProvider that offloads
|
|
5
|
+
* embedding computation to a dedicated worker thread.
|
|
6
|
+
* The main thread's event loop stays free for serving searches.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const proxy = new EmbeddingWorkerProxy('local', { model: '...' });
|
|
10
|
+
* await proxy.ready();
|
|
11
|
+
* const vec = await proxy.embed('hello');
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EmbeddingProvider } from '@/types.ts';
|
|
15
|
+
|
|
16
|
+
import { Worker } from 'node:worker_threads';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
|
|
20
|
+
/** Message sent to the worker. */
|
|
21
|
+
interface WorkerRequest {
|
|
22
|
+
id: number;
|
|
23
|
+
type: 'embed' | 'embedBatch' | 'close';
|
|
24
|
+
text?: string;
|
|
25
|
+
texts?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Message received from the worker. */
|
|
29
|
+
interface WorkerResponse {
|
|
30
|
+
id: number;
|
|
31
|
+
type: 'result' | 'error';
|
|
32
|
+
vector?: ArrayBuffer;
|
|
33
|
+
vectors?: ArrayBuffer[];
|
|
34
|
+
dims?: number;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class EmbeddingWorkerProxy implements EmbeddingProvider {
|
|
39
|
+
private _worker: Worker;
|
|
40
|
+
private _nextId = 1;
|
|
41
|
+
private _pending = new Map<number, { resolve: (v: WorkerResponse) => void; reject: (e: Error) => void }>();
|
|
42
|
+
private _ready: Promise<void>;
|
|
43
|
+
private _dims = 0;
|
|
44
|
+
|
|
45
|
+
/** Embedding dimensions (available after `ready()` resolves). */
|
|
46
|
+
get dims(): number { return this._dims; }
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
providerType: string,
|
|
50
|
+
providerOptions: Record<string, unknown> = {},
|
|
51
|
+
) {
|
|
52
|
+
const workerPath = join(
|
|
53
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
54
|
+
'embedding-worker-thread.ts',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
this._worker = new Worker(workerPath, {
|
|
58
|
+
workerData: { providerType, providerOptions },
|
|
59
|
+
execArgv: ['--import', 'tsx'],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this._worker.on('message', (msg: WorkerResponse) => {
|
|
63
|
+
// Initial ready signal (id === 0)
|
|
64
|
+
if (msg.id === 0 && msg.dims) {
|
|
65
|
+
this._dims = msg.dims;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pending = this._pending.get(msg.id);
|
|
70
|
+
if (!pending) return;
|
|
71
|
+
this._pending.delete(msg.id);
|
|
72
|
+
|
|
73
|
+
if (msg.type === 'error') {
|
|
74
|
+
pending.reject(new Error(`BrainBank: Embedding worker error: ${msg.error}`));
|
|
75
|
+
} else {
|
|
76
|
+
pending.resolve(msg);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this._worker.on('error', (err: Error) => {
|
|
81
|
+
for (const pending of this._pending.values()) {
|
|
82
|
+
pending.reject(err);
|
|
83
|
+
}
|
|
84
|
+
this._pending.clear();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Wait for the worker to signal ready
|
|
88
|
+
this._ready = new Promise<void>((resolve, reject) => {
|
|
89
|
+
const onMsg = (msg: WorkerResponse) => {
|
|
90
|
+
if (msg.id === 0 && msg.dims) {
|
|
91
|
+
this._dims = msg.dims;
|
|
92
|
+
this._worker.removeListener('message', onMsg);
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this._worker.on('message', onMsg);
|
|
97
|
+
this._worker.on('error', reject);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Wait for the worker to be ready (provider loaded). */
|
|
102
|
+
async ready(): Promise<void> {
|
|
103
|
+
return this._ready;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Send a request to the worker and wait for the response. */
|
|
107
|
+
private _send(req: Omit<WorkerRequest, 'id'>): Promise<WorkerResponse> {
|
|
108
|
+
const id = this._nextId++;
|
|
109
|
+
return new Promise<WorkerResponse>((resolve, reject) => {
|
|
110
|
+
this._pending.set(id, { resolve, reject });
|
|
111
|
+
this._worker.postMessage({ ...req, id } satisfies WorkerRequest);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Embed a single text string. */
|
|
116
|
+
async embed(text: string): Promise<Float32Array> {
|
|
117
|
+
await this._ready;
|
|
118
|
+
const resp = await this._send({ type: 'embed', text });
|
|
119
|
+
if (!resp.vector) throw new Error('BrainBank: Worker returned no vector.');
|
|
120
|
+
return new Float32Array(resp.vector);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Embed multiple texts in a batch. */
|
|
124
|
+
async embedBatch(texts: string[]): Promise<Float32Array[]> {
|
|
125
|
+
if (texts.length === 0) return [];
|
|
126
|
+
await this._ready;
|
|
127
|
+
const resp = await this._send({ type: 'embedBatch', texts });
|
|
128
|
+
if (!resp.vectors) throw new Error('BrainBank: Worker returned no vectors.');
|
|
129
|
+
return resp.vectors.map(buf => new Float32Array(buf));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Terminate the worker. */
|
|
133
|
+
async close(): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
await this._send({ type: 'close' });
|
|
136
|
+
} catch {
|
|
137
|
+
// Worker may already be terminated
|
|
138
|
+
}
|
|
139
|
+
await this._worker.terminate();
|
|
140
|
+
}
|
|
141
|
+
}
|