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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/assets/architecture.png +0 -0
  4. package/bin/brainbank +18 -0
  5. package/bin/brainbank-mcp +19 -0
  6. package/dist/chunk-3YBCD6DI.js +117 -0
  7. package/dist/chunk-3YBCD6DI.js.map +1 -0
  8. package/dist/chunk-63GBCDS5.js +3249 -0
  9. package/dist/chunk-63GBCDS5.js.map +1 -0
  10. package/dist/chunk-DMFMTOHF.js +123 -0
  11. package/dist/chunk-DMFMTOHF.js.map +1 -0
  12. package/dist/chunk-FQYKWB2Q.js +136 -0
  13. package/dist/chunk-FQYKWB2Q.js.map +1 -0
  14. package/dist/chunk-IMJJ2VEM.js +74 -0
  15. package/dist/chunk-IMJJ2VEM.js.map +1 -0
  16. package/dist/chunk-M744PCJQ.js +43 -0
  17. package/dist/chunk-M744PCJQ.js.map +1 -0
  18. package/dist/chunk-O3J6ZIXK.js +82 -0
  19. package/dist/chunk-O3J6ZIXK.js.map +1 -0
  20. package/dist/chunk-OPH7GZ7U.js +124 -0
  21. package/dist/chunk-OPH7GZ7U.js.map +1 -0
  22. package/dist/chunk-PXEWQMN7.js +89 -0
  23. package/dist/chunk-PXEWQMN7.js.map +1 -0
  24. package/dist/chunk-RDQYDLYZ.js +69 -0
  25. package/dist/chunk-RDQYDLYZ.js.map +1 -0
  26. package/dist/chunk-VIIHPCC4.js +254 -0
  27. package/dist/chunk-VIIHPCC4.js.map +1 -0
  28. package/dist/chunk-WCQVDF3K.js +14 -0
  29. package/dist/chunk-WCQVDF3K.js.map +1 -0
  30. package/dist/cli.d.ts +1 -0
  31. package/dist/cli.js +3076 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/haiku-expander-YRSIPGKP.js +8 -0
  34. package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
  35. package/dist/haiku-pruner-SHAXUPY6.js +8 -0
  36. package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
  37. package/dist/http-server-QUXHLWUM.js +9 -0
  38. package/dist/http-server-QUXHLWUM.js.map +1 -0
  39. package/dist/index.d.ts +2161 -0
  40. package/dist/index.js +357 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/local-embedding-NZQTILGV.js +8 -0
  43. package/dist/local-embedding-NZQTILGV.js.map +1 -0
  44. package/dist/mcp.d.ts +2 -0
  45. package/dist/mcp.js +334 -0
  46. package/dist/mcp.js.map +1 -0
  47. package/dist/openai-embedding-ZP5TSUJG.js +8 -0
  48. package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
  49. package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
  50. package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
  51. package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
  52. package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
  53. package/dist/plugin-IKQ6IRSJ.js +32 -0
  54. package/dist/plugin-IKQ6IRSJ.js.map +1 -0
  55. package/dist/resolve-ASGLBNUC.js +10 -0
  56. package/dist/resolve-ASGLBNUC.js.map +1 -0
  57. package/dist/stats-tui-ZY2NQSEA.js +1904 -0
  58. package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
  59. package/package.json +96 -0
  60. package/src/brainbank.ts +617 -0
  61. package/src/cli/commands/collection.ts +77 -0
  62. package/src/cli/commands/context.ts +179 -0
  63. package/src/cli/commands/daemon.ts +100 -0
  64. package/src/cli/commands/docs.ts +71 -0
  65. package/src/cli/commands/files.ts +69 -0
  66. package/src/cli/commands/help.ts +77 -0
  67. package/src/cli/commands/index.ts +482 -0
  68. package/src/cli/commands/kv.ts +140 -0
  69. package/src/cli/commands/mcp-export.ts +273 -0
  70. package/src/cli/commands/mcp.ts +6 -0
  71. package/src/cli/commands/reembed.ts +30 -0
  72. package/src/cli/commands/scan.ts +336 -0
  73. package/src/cli/commands/search.ts +203 -0
  74. package/src/cli/commands/stats.ts +68 -0
  75. package/src/cli/commands/status.ts +47 -0
  76. package/src/cli/commands/watch.ts +47 -0
  77. package/src/cli/factory/brain-context.ts +43 -0
  78. package/src/cli/factory/builtin-registration.ts +87 -0
  79. package/src/cli/factory/config-loader.ts +77 -0
  80. package/src/cli/factory/index.ts +69 -0
  81. package/src/cli/factory/plugin-loader.ts +325 -0
  82. package/src/cli/index.ts +71 -0
  83. package/src/cli/server-client.ts +178 -0
  84. package/src/cli/tui/index-tui.tsx +667 -0
  85. package/src/cli/tui/stats-data.ts +523 -0
  86. package/src/cli/tui/stats-search.ts +262 -0
  87. package/src/cli/tui/stats-tui.tsx +1465 -0
  88. package/src/cli/tui/tree-scanner.ts +650 -0
  89. package/src/cli/utils.ts +137 -0
  90. package/src/config.ts +49 -0
  91. package/src/constants.ts +21 -0
  92. package/src/db/adapter.ts +112 -0
  93. package/src/db/metadata.ts +130 -0
  94. package/src/db/migrations.ts +66 -0
  95. package/src/db/sqlite-adapter.ts +218 -0
  96. package/src/db/tracker.ts +91 -0
  97. package/src/engine/index-api.ts +81 -0
  98. package/src/engine/reembed.ts +206 -0
  99. package/src/engine/search-api.ts +218 -0
  100. package/src/index.ts +154 -0
  101. package/src/lib/fts.ts +57 -0
  102. package/src/lib/languages.ts +180 -0
  103. package/src/lib/logger.ts +126 -0
  104. package/src/lib/math.ts +87 -0
  105. package/src/lib/provider-key.ts +20 -0
  106. package/src/lib/prune.ts +71 -0
  107. package/src/lib/rrf.ts +133 -0
  108. package/src/lib/write-lock.ts +108 -0
  109. package/src/mcp/mcp-server.ts +195 -0
  110. package/src/mcp/workspace-factory.ts +68 -0
  111. package/src/mcp/workspace-pool.ts +224 -0
  112. package/src/plugin.ts +381 -0
  113. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  114. package/src/providers/embeddings/embedding-worker.ts +141 -0
  115. package/src/providers/embeddings/local-embedding.ts +115 -0
  116. package/src/providers/embeddings/openai-embedding.ts +167 -0
  117. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  118. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  119. package/src/providers/embeddings/resolve.ts +34 -0
  120. package/src/providers/pruners/haiku-expander.ts +166 -0
  121. package/src/providers/pruners/haiku-pruner.ts +112 -0
  122. package/src/providers/vector/hnsw-index.ts +174 -0
  123. package/src/providers/vector/hnsw-loader.ts +129 -0
  124. package/src/search/bm25-boost.ts +69 -0
  125. package/src/search/context-builder.ts +251 -0
  126. package/src/search/keyword/composite-bm25-search.ts +47 -0
  127. package/src/search/types.ts +37 -0
  128. package/src/search/vector/composite-vector-search.ts +61 -0
  129. package/src/search/vector/mmr.ts +64 -0
  130. package/src/services/collection.ts +384 -0
  131. package/src/services/daemon.ts +87 -0
  132. package/src/services/http-server.ts +336 -0
  133. package/src/services/kv-service.ts +64 -0
  134. package/src/services/plugin-registry.ts +77 -0
  135. package/src/services/watch.ts +340 -0
  136. package/src/services/webhook-server.ts +100 -0
  137. package/src/types.ts +493 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * BrainBank CLI — Plugin Loader
3
+ *
4
+ * Generic plugin loader registry with dynamic @brainbank/* package loading,
5
+ * npm package fallback for third-party plugins, and auto-discovery of
6
+ * user plugins from .brainbank/plugins/.
7
+ */
8
+
9
+ import type { Plugin, PluginScanInfo, PluginPreviewLine } from '@/plugin.ts';
10
+ import type { EmbeddingProvider } from '@/types.ts';
11
+ import type { ProjectConfig } from './config-loader.ts';
12
+ import type { ScanModule } from '@/cli/commands/scan.ts';
13
+ import type { PreviewLine } from '@/cli/tui/tree-scanner.ts';
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { c } from '../utils.ts';
18
+
19
+ /** Plugin factory — accepts config, returns Plugin. */
20
+ type PluginFactory = (opts: Record<string, unknown>) => Plugin;
21
+
22
+ /** Loader function: dynamically imports a package and returns its factory. */
23
+ type PluginLoaderFn = () => Promise<PluginFactory | null>;
24
+
25
+ /** Built-in plugin loader registry. Extensible at runtime. */
26
+ const PLUGIN_LOADERS = new Map<string, PluginLoaderFn>([
27
+ ['code', async () => { try { return (await import('@brainbank/code')).code as PluginFactory; } catch { return null; } }],
28
+ ['git', async () => { try { return (await import('@brainbank/git')).git as PluginFactory; } catch { return null; } }],
29
+ ['docs', async () => { try { return (await import('@brainbank/docs')).docs as PluginFactory; } catch { return null; } }],
30
+ ]);
31
+
32
+ /** Built-in plugin names — used to skip npm fallback. */
33
+ const BUILTIN_PLUGINS = new Set(['code', 'git', 'docs']);
34
+
35
+
36
+
37
+ /**
38
+ * Load a plugin factory by name.
39
+ * 1. Check the built-in loader registry.
40
+ * 2. Fallback: try `import(name)` for npm packages (third-party plugins).
41
+ * Returns null if not installed.
42
+ */
43
+ export async function loadPlugin(name: string): Promise<PluginFactory | null> {
44
+ // Built-in loader
45
+ const loader = PLUGIN_LOADERS.get(name);
46
+ if (loader) return loader();
47
+
48
+ // npm package fallback — try importing the package directly
49
+ try {
50
+ const mod = await import(name) as Record<string, unknown>;
51
+ // Convention: export default factory, or named export matching short name
52
+ // e.g. 'brainbank-csv' exports `csv()`, '@myorg/brainbank-csv' exports `csv()`
53
+ const shortName = name.replace(/^@[^/]+\//, '').replace(/^brainbank-/, '');
54
+ const factory = mod.default ?? mod[shortName];
55
+ if (typeof factory === 'function') return factory as PluginFactory;
56
+
57
+ // Fallback: check all exports for a function that returns a Plugin-like object
58
+ for (const val of Object.values(mod)) {
59
+ if (typeof val === 'function') return val as PluginFactory;
60
+ }
61
+ } catch {
62
+ // Not installed — will be reported by registerBuiltins
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ /** Register a custom plugin loader. */
69
+ export function registerPluginLoader(name: string, loader: PluginLoaderFn): void {
70
+ PLUGIN_LOADERS.set(name, loader);
71
+ }
72
+
73
+
74
+
75
+ /** Check if a plugin name is a built-in. */
76
+ export function isBuiltinPlugin(name: string): boolean {
77
+ return BUILTIN_PLUGINS.has(name);
78
+ }
79
+
80
+ const INDEXER_EXTENSIONS = ['.ts', '.js', '.mjs'];
81
+ const NOT_LOADED = Symbol('not-loaded');
82
+ let _folderPluginsCache: Plugin[] | typeof NOT_LOADED = NOT_LOADED;
83
+
84
+ /** Auto-discover plugins from .brainbank/plugins/ folder. */
85
+ export async function discoverFolderPlugins(repoPath: string): Promise<Plugin[]> {
86
+ if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
87
+
88
+ const pluginsDir = path.resolve(repoPath, '.brainbank', 'plugins');
89
+
90
+ if (!fs.existsSync(pluginsDir)) {
91
+ _folderPluginsCache = [];
92
+ return [];
93
+ }
94
+
95
+ const files = fs.readdirSync(pluginsDir)
96
+ .filter(f => INDEXER_EXTENSIONS.some(ext => f.endsWith(ext)))
97
+ .sort();
98
+
99
+ const plugins: Plugin[] = [];
100
+
101
+ for (const file of files) {
102
+ const filePath = path.join(pluginsDir, file);
103
+ try {
104
+ const mod = await import(filePath);
105
+ const plugin = mod.default ?? mod;
106
+
107
+ if (plugin && typeof plugin === 'object' && plugin.name) {
108
+ plugins.push(plugin as Plugin);
109
+ } else {
110
+ console.error(c.yellow(`⚠ ${file}: must export a default Plugin with a 'name' property, skipping`));
111
+ }
112
+ } catch (err: unknown) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ console.error(c.red(`Error loading plugin ${file}: ${message}`));
115
+ }
116
+ }
117
+
118
+ _folderPluginsCache = plugins;
119
+ return plugins;
120
+ }
121
+
122
+ /** Reset folder plugins cache. Useful for tests. */
123
+ export function resetPluginCache(): void {
124
+ _folderPluginsCache = NOT_LOADED;
125
+ }
126
+
127
+
128
+ // ── External Plugin Discovery (TUI integration) ──────────────────
129
+
130
+ /** Result of discovering external (non-built-in) plugins. */
131
+ export interface ExternalPluginDiscovery {
132
+ /** ScanModule entries to merge into the TUI sidebar. */
133
+ modules: ScanModule[];
134
+ /** Preview lines keyed by plugin name, for the TUI explorer panel. */
135
+ previews: Map<string, PreviewLine[]>;
136
+ }
137
+
138
+ /**
139
+ * Discover external plugins from config and produce scan/preview data for the TUI.
140
+ *
141
+ * For each non-built-in plugin in the list:
142
+ * 1. Try `import(name)` to load the package.
143
+ * 2. If it exports `scan(repoPath)`, use it for the sidebar.
144
+ * 3. If it exports `preview(repoPath)`, use it for the explorer panel.
145
+ * 4. If not installed, show as unavailable with install hint.
146
+ *
147
+ * Also scans `.brainbank/plugins/` folder plugins for scan/preview exports.
148
+ */
149
+ export async function discoverExternalPlugins(
150
+ repoPath: string,
151
+ pluginNames: string[],
152
+ ): Promise<ExternalPluginDiscovery> {
153
+ const modules: ScanModule[] = [];
154
+ const previews = new Map<string, PreviewLine[]>();
155
+ const resolvedRp = path.resolve(repoPath);
156
+
157
+ // 1. Discover npm plugins listed in config
158
+ for (const name of pluginNames) {
159
+ if (BUILTIN_PLUGINS.has(name)) continue;
160
+
161
+ try {
162
+ const mod = await import(name) as Record<string, unknown>;
163
+
164
+ // scan() → ScanModule for sidebar
165
+ if (typeof mod.scan === 'function') {
166
+ const info = mod.scan(resolvedRp) as PluginScanInfo;
167
+ modules.push(scanInfoToModule(info));
168
+ } else {
169
+ // No scan export — generic entry
170
+ modules.push({
171
+ name,
172
+ available: true,
173
+ checked: true,
174
+ icon: '🔌',
175
+ summary: `${name} plugin`,
176
+ });
177
+ }
178
+
179
+ // preview() → PreviewLine[] for explorer
180
+ if (typeof mod.preview === 'function') {
181
+ const lines = mod.preview(resolvedRp) as PluginPreviewLine[];
182
+ previews.set(name, lines.map(previewLineToInternal));
183
+ }
184
+ } catch {
185
+ // Package not installed
186
+ modules.push({
187
+ name,
188
+ available: false,
189
+ checked: false,
190
+ icon: '🔌',
191
+ summary: 'not installed',
192
+ disabled: `npm i ${name}`,
193
+ });
194
+ }
195
+ }
196
+
197
+ // 2. Discover folder plugins that export scan/preview
198
+ const pluginsDir = path.resolve(repoPath, '.brainbank', 'plugins');
199
+ if (fs.existsSync(pluginsDir)) {
200
+ const files = fs.readdirSync(pluginsDir)
201
+ .filter(f => INDEXER_EXTENSIONS.some(ext => f.endsWith(ext)))
202
+ .sort();
203
+
204
+ for (const file of files) {
205
+ const filePath = path.join(pluginsDir, file);
206
+ try {
207
+ const mod = await import(filePath) as Record<string, unknown>;
208
+ const plugin = mod.default ?? mod;
209
+ const pluginName = (plugin && typeof plugin === 'object' && 'name' in plugin)
210
+ ? (plugin as { name: string }).name
211
+ : file.replace(/\.[^.]+$/, '');
212
+
213
+ // Skip if already added from config
214
+ if (modules.some(m => m.name === pluginName)) continue;
215
+
216
+ if (typeof mod.scan === 'function') {
217
+ const info = mod.scan(resolvedRp) as PluginScanInfo;
218
+ modules.push(scanInfoToModule(info));
219
+ } else {
220
+ modules.push({
221
+ name: pluginName,
222
+ available: true,
223
+ checked: true,
224
+ icon: '🔌',
225
+ summary: `local plugin (${file})`,
226
+ });
227
+ }
228
+
229
+ if (typeof mod.preview === 'function') {
230
+ const lines = mod.preview(resolvedRp) as PluginPreviewLine[];
231
+ previews.set(pluginName, lines.map(previewLineToInternal));
232
+ }
233
+ } catch {
234
+ // Failed to load folder plugin — skip for discovery
235
+ }
236
+ }
237
+ }
238
+
239
+ return { modules, previews };
240
+ }
241
+
242
+ /** Convert PluginScanInfo (public type) to ScanModule (internal type). */
243
+ function scanInfoToModule(info: PluginScanInfo): ScanModule {
244
+ return {
245
+ name: info.name,
246
+ available: info.available,
247
+ summary: info.summary,
248
+ icon: info.icon,
249
+ checked: info.checked,
250
+ disabled: info.disabled,
251
+ details: info.details,
252
+ };
253
+ }
254
+
255
+ /** Convert PluginPreviewLine (public type) to PreviewLine (internal type). */
256
+ function previewLineToInternal(line: PluginPreviewLine): PreviewLine {
257
+ return {
258
+ text: line.text,
259
+ color: line.color,
260
+ bold: line.bold,
261
+ dim: line.dim,
262
+ };
263
+ }
264
+
265
+
266
+ // ── Provider Setup ───────────────────────────────────────────────
267
+
268
+ /** Resolve an embedding key string to an EmbeddingProvider instance. */
269
+ export async function resolveEmbeddingKey(key: string): Promise<EmbeddingProvider> {
270
+ const { resolveEmbedding } = await import('@/providers/embeddings/resolve.ts');
271
+ return resolveEmbedding(key);
272
+ }
273
+
274
+ /** Configure pruner, expander, and global embedding provider on brainOpts. */
275
+ export async function setupProviders(
276
+ brainOpts: Record<string, unknown>,
277
+ config: ProjectConfig | null,
278
+ flags?: Record<string, string | undefined>,
279
+ env?: Record<string, string | undefined>,
280
+ ): Promise<void> {
281
+ // Resolve API keys: config.keys > env vars
282
+ const keys = config?.keys ?? {};
283
+ const anthropicKey = keys.anthropic || process.env.ANTHROPIC_API_KEY;
284
+ const perplexityKey = keys.perplexity || process.env.PERPLEXITY_API_KEY;
285
+ const openaiKey = keys.openai || process.env.OPENAI_API_KEY;
286
+
287
+ // Inject resolved keys into process.env so downstream providers auto-detect them
288
+ if (anthropicKey) process.env.ANTHROPIC_API_KEY = anthropicKey;
289
+ if (perplexityKey) process.env.PERPLEXITY_API_KEY = perplexityKey;
290
+ if (openaiKey) process.env.OPENAI_API_KEY = openaiKey;
291
+
292
+
293
+
294
+ const prunerFlag = flags?.pruner ?? (config?.pruner as string | undefined);
295
+ if (prunerFlag === 'haiku') {
296
+ const { HaikuPruner } = await import('@/providers/pruners/haiku-pruner.ts');
297
+ brainOpts.pruner = new HaikuPruner({ apiKey: anthropicKey });
298
+ }
299
+
300
+ // Expander: explicit opt-in only (config.json `expander: "haiku"` or --expander flag)
301
+ const expanderFlag = flags?.expander ?? (config?.expander as string | undefined);
302
+ if (expanderFlag === 'haiku') {
303
+ try {
304
+ const { HaikuExpander } = await import('@/providers/pruners/haiku-expander.ts');
305
+ brainOpts.expander = new HaikuExpander({ apiKey: anthropicKey });
306
+ } catch {
307
+ // Fail-open: if API key missing, skip expander silently
308
+ }
309
+ }
310
+
311
+ const embFlag = flags?.embedding
312
+ ?? (config?.embedding as string | undefined)
313
+ ?? env?.BRAINBANK_EMBEDDING
314
+ ?? process.env.BRAINBANK_EMBEDDING;
315
+ if (embFlag) {
316
+ const provider = await resolveEmbeddingKey(embFlag);
317
+ brainOpts.embeddingProvider = provider;
318
+ brainOpts.embeddingDims = provider.dims;
319
+ }
320
+
321
+ // Context field defaults from config.json "context" section
322
+ if (config?.context) {
323
+ brainOpts.contextFields = config.context;
324
+ }
325
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+
4
+ /**
5
+ * BrainBank — CLI Entry Point
6
+ *
7
+ * Dispatcher that routes commands to their handler modules.
8
+ */
9
+
10
+ import { args, c } from './utils.ts';
11
+ import { cmdIndex } from './commands/index.ts';
12
+ import { cmdCollection } from './commands/collection.ts';
13
+ import { cmdKv } from './commands/kv.ts';
14
+ import { cmdDocs, cmdDocSearch } from './commands/docs.ts';
15
+ import { cmdSearch, cmdHybridSearch, cmdKeywordSearch } from './commands/search.ts';
16
+ import { cmdContext } from './commands/context.ts';
17
+ import { cmdFiles } from './commands/files.ts';
18
+ import { cmdStats } from './commands/stats.ts';
19
+ import { cmdReembed } from './commands/reembed.ts';
20
+ import { cmdWatch } from './commands/watch.ts';
21
+ import { cmdMcp } from './commands/mcp.ts';
22
+ import { cmdMcpExport } from './commands/mcp-export.ts';
23
+ import { cmdDaemon } from './commands/daemon.ts';
24
+ import { cmdStatus } from './commands/status.ts';
25
+ import { showHelp } from './commands/help.ts';
26
+ import { VERSION } from '@/constants.ts';
27
+
28
+ const command = args[0];
29
+
30
+ async function main(): Promise<void> {
31
+ switch (command) {
32
+ case '--version':
33
+ case '-v':
34
+ console.log(`brainbank v${VERSION}`);
35
+ break;
36
+ case 'i':
37
+ case 'index': return cmdIndex();
38
+ case 'collection': return cmdCollection();
39
+ case 'kv': return cmdKv();
40
+ case 'docs': return cmdDocs();
41
+ case 'dsearch': return cmdDocSearch();
42
+ case 'search': return cmdSearch();
43
+ case 'hsearch': return cmdHybridSearch();
44
+ case 'ksearch': return cmdKeywordSearch();
45
+ case 'context': return cmdContext();
46
+ case 'files': return cmdFiles();
47
+ case 'stats': return cmdStats();
48
+ case 'reembed': return cmdReembed();
49
+ case 'watch': return cmdWatch();
50
+ case 'mcp': return cmdMcp();
51
+ case 'mcp:export': return cmdMcpExport();
52
+ case 'serve': return cmdMcp(); // backward compat
53
+ case 'daemon': return cmdDaemon();
54
+ case 'status': return cmdStatus();
55
+ case 'help':
56
+ case '--help':
57
+ case '-h':
58
+ showHelp();
59
+ break;
60
+ default:
61
+ if (command) console.log(c.red(`Unknown command: ${command}\n`));
62
+ showHelp();
63
+ process.exit(command ? 1 : 0);
64
+ }
65
+ }
66
+
67
+ main().catch(err => {
68
+ console.error(c.red(`Error: ${err.message}`));
69
+ if (process.env.BRAINBANK_DEBUG) console.error(err.stack);
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ServerClient — Lightweight HTTP client for the BrainBank daemon.
3
+ *
4
+ * Used by CLI commands to delegate to a running HTTP server
5
+ * instead of loading models locally. Falls back gracefully
6
+ * (returns null) if the server is unreachable.
7
+ */
8
+
9
+ import type { SearchResult } from '@/types.ts';
10
+
11
+ import * as http from 'node:http';
12
+
13
+ import { isServerRunning } from '@/services/daemon.ts';
14
+
15
+ interface ContextOptions {
16
+ task: string;
17
+ repo?: string;
18
+ sources?: Record<string, number>;
19
+ pathPrefix?: string | string[];
20
+ affectedFiles?: string[];
21
+ }
22
+
23
+ /**
24
+ * Try to get context from the running HTTP server.
25
+ * Returns the context string if successful, null if server is unreachable.
26
+ */
27
+ export async function tryServerContext(options: ContextOptions): Promise<string | null> {
28
+ const info = isServerRunning();
29
+ if (!info) return null;
30
+
31
+ try {
32
+ const body = JSON.stringify({
33
+ task: options.task,
34
+ repo: options.repo,
35
+ sources: options.sources,
36
+ pathPrefix: options.pathPrefix,
37
+ affectedFiles: options.affectedFiles,
38
+ });
39
+
40
+ const response = await httpPost(info.port, '/context', body);
41
+ const data = JSON.parse(response) as { context?: string; error?: string };
42
+
43
+ if (data.error) return null;
44
+ return data.context ?? null;
45
+ } catch {
46
+ // Server unreachable or error — fall back to local
47
+ return null;
48
+ }
49
+ }
50
+
51
+ interface SearchDelegateOptions {
52
+ query: string;
53
+ repo?: string;
54
+ sources?: Record<string, number>;
55
+ pathPrefix?: string | string[];
56
+ maxResults?: number;
57
+ }
58
+
59
+ /**
60
+ * Try to delegate a search to the running HTTP server.
61
+ * Mode: 'search' (vector), 'hybrid' (hsearch), 'keyword' (ksearch).
62
+ * Returns SearchResult[] if successful, null if server is unreachable.
63
+ */
64
+ export async function tryServerSearch(
65
+ mode: 'search' | 'hybrid' | 'keyword',
66
+ options: SearchDelegateOptions,
67
+ ): Promise<SearchResult[] | null> {
68
+ const info = isServerRunning();
69
+ if (!info) return null;
70
+
71
+ const endpoint = mode === 'search' ? '/search' : mode === 'hybrid' ? '/hsearch' : '/ksearch';
72
+
73
+ try {
74
+ const body = JSON.stringify({
75
+ query: options.query,
76
+ repo: options.repo,
77
+ sources: options.sources,
78
+ pathPrefix: options.pathPrefix,
79
+ maxResults: options.maxResults,
80
+ });
81
+
82
+ const response = await httpPost(info.port, endpoint, body);
83
+ const data = JSON.parse(response) as { results?: SearchResult[]; error?: string };
84
+
85
+ if (data.error) return null;
86
+ return data.results ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Try to trigger indexing on the running HTTP server.
94
+ * Returns the result if successful, null if server is unreachable.
95
+ */
96
+ export async function tryServerIndex(repo?: string, forceReindex?: boolean): Promise<Record<string, unknown> | null> {
97
+ const info = isServerRunning();
98
+ if (!info) return null;
99
+
100
+ try {
101
+ const body = JSON.stringify({ repo, forceReindex });
102
+ const response = await httpPost(info.port, '/index', body);
103
+ const data = JSON.parse(response) as { result?: Record<string, unknown>; error?: string };
104
+
105
+ if (data.error) return null;
106
+ return data.result ?? null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check server health. Returns health info or null.
114
+ */
115
+ export async function serverHealth(): Promise<{
116
+ ok: boolean;
117
+ pid: number;
118
+ port: number;
119
+ uptime: number;
120
+ workspaces: number;
121
+ } | null> {
122
+ const info = isServerRunning();
123
+ if (!info) return null;
124
+
125
+ try {
126
+ const response = await httpGet(info.port, '/health');
127
+ return JSON.parse(response) as { ok: boolean; pid: number; port: number; uptime: number; workspaces: number };
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // ── HTTP helpers ────────────────────────────────────
134
+
135
+ function httpPost(port: number, path: string, body: string): Promise<string> {
136
+ return new Promise((resolve, reject) => {
137
+ const req = http.request({
138
+ hostname: '127.0.0.1',
139
+ port,
140
+ path,
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ 'Content-Length': Buffer.byteLength(body),
145
+ },
146
+ timeout: 120_000, // 2 minutes — context queries can be slow on first load
147
+ }, (res) => {
148
+ const chunks: Buffer[] = [];
149
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
150
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
151
+ });
152
+
153
+ req.on('error', reject);
154
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
155
+ req.write(body);
156
+ req.end();
157
+ });
158
+ }
159
+
160
+ function httpGet(port: number, path: string): Promise<string> {
161
+ return new Promise((resolve, reject) => {
162
+ const req = http.request({
163
+ hostname: '127.0.0.1',
164
+ port,
165
+ path,
166
+ method: 'GET',
167
+ timeout: 5_000,
168
+ }, (res) => {
169
+ const chunks: Buffer[] = [];
170
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
171
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
172
+ });
173
+
174
+ req.on('error', reject);
175
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
176
+ req.end();
177
+ });
178
+ }