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,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank index [path] — Interactive scan → select → index
|
|
3
|
+
*
|
|
4
|
+
* Scans the repo first, shows an interactive TUI with directory tree
|
|
5
|
+
* for folder selection, then indexes. Use --yes to skip the TUI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ScanResult, ScanModule } from './scan.ts';
|
|
9
|
+
import type { PreviewLine } from '@/cli/tui/tree-scanner.ts';
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { c, args, getFlag, hasFlag, stripFlags } from '@/cli/utils.ts';
|
|
14
|
+
import { createBrain, getConfig, registerConfigCollections, contextFromCLI } from '@/cli/factory/index.ts';
|
|
15
|
+
import { discoverExternalPlugins } from '@/cli/factory/plugin-loader.ts';
|
|
16
|
+
import { findDocsPlugin } from '@/cli/utils.ts';
|
|
17
|
+
import { autoExportMcp } from './mcp-export.ts';
|
|
18
|
+
import { scanRepo } from './scan.ts';
|
|
19
|
+
import { runIndexTui } from '@/cli/tui/index-tui.tsx';
|
|
20
|
+
|
|
21
|
+
export async function cmdIndex(): Promise<void> {
|
|
22
|
+
const positional = stripFlags(args);
|
|
23
|
+
const repoPath = positional[1] || '.';
|
|
24
|
+
const force = hasFlag('force');
|
|
25
|
+
const depth = parseInt(getFlag('depth') || '500', 10);
|
|
26
|
+
const onlyRaw = getFlag('only');
|
|
27
|
+
const docsPath = getFlag('docs');
|
|
28
|
+
const skipPrompt = hasFlag('yes') || hasFlag('y');
|
|
29
|
+
const forceSetup = hasFlag('setup');
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const scan = scanRepo(repoPath);
|
|
33
|
+
|
|
34
|
+
// Discover external (non-built-in) plugins from config and .brainbank/plugins/
|
|
35
|
+
const configPlugins = scan.config.plugins ?? [];
|
|
36
|
+
const externalDiscovery = await discoverExternalPlugins(repoPath, configPlugins);
|
|
37
|
+
let externalPreviews: Map<string, PreviewLine[]> = externalDiscovery.previews;
|
|
38
|
+
|
|
39
|
+
// Merge external modules into scan result
|
|
40
|
+
if (externalDiscovery.modules.length > 0) {
|
|
41
|
+
scan.modules = [...scan.modules, ...externalDiscovery.modules];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let modules: string[];
|
|
45
|
+
let tuiInclude: string[] = [];
|
|
46
|
+
let tuiIgnore: string[] = [];
|
|
47
|
+
let tuiConfig: { embedding: string; pruner: string; expander: string } | undefined;
|
|
48
|
+
|
|
49
|
+
if (onlyRaw) {
|
|
50
|
+
// --only flag: explicit module selection
|
|
51
|
+
printIndexHeader(scan, depth);
|
|
52
|
+
modules = onlyRaw.split(',').map(s => s.trim());
|
|
53
|
+
} else if (scan.config.plugins && scan.config.plugins.length > 0 && !forceSetup) {
|
|
54
|
+
// Config exists with plugins field — skip TUI, index directly
|
|
55
|
+
printIndexHeader(scan, depth);
|
|
56
|
+
modules = scan.config.plugins;
|
|
57
|
+
} else if (skipPrompt) {
|
|
58
|
+
printIndexHeader(scan, depth);
|
|
59
|
+
modules = buildDefaultModules(scan);
|
|
60
|
+
} else {
|
|
61
|
+
// ── Interactive TUI ──
|
|
62
|
+
const selection = await runIndexTui(scan, externalPreviews);
|
|
63
|
+
if (!selection) {
|
|
64
|
+
console.log(c.dim('\n Cancelled. Exiting.\n'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
modules = selection.modules;
|
|
68
|
+
tuiInclude = selection.include;
|
|
69
|
+
tuiIgnore = selection.ignore;
|
|
70
|
+
tuiConfig = selection.config;
|
|
71
|
+
|
|
72
|
+
if (modules.length === 0) {
|
|
73
|
+
console.log(c.dim('\n Nothing selected. Exiting.\n'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Deindex removed modules ──
|
|
78
|
+
const oldPlugins = scan.config.plugins ?? [];
|
|
79
|
+
const removed = oldPlugins.filter(p => !modules.includes(p));
|
|
80
|
+
if (removed.length > 0) {
|
|
81
|
+
console.log(c.bold('\n━━━ Deindexing ━━━\n'));
|
|
82
|
+
for (const mod of removed) {
|
|
83
|
+
console.log(` ${c.yellow('✗')} Removing ${mod} data...`);
|
|
84
|
+
deindexModule(scan.repoPath, mod);
|
|
85
|
+
console.log(` ${c.green('✓')} ${mod} data cleared`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Show selection summary
|
|
90
|
+
console.log(c.bold('\n━━━ BrainBank ━━━\n'));
|
|
91
|
+
console.log(' Selected modules:');
|
|
92
|
+
for (const m of modules) {
|
|
93
|
+
console.log(` ${c.green('✓')} ${m}`);
|
|
94
|
+
}
|
|
95
|
+
if (tuiInclude.length > 0) {
|
|
96
|
+
console.log(` Include: ${c.cyan(tuiInclude.join(', '))}`);
|
|
97
|
+
}
|
|
98
|
+
if (tuiIgnore.length > 0) {
|
|
99
|
+
console.log(` Ignore: ${c.yellow(tuiIgnore.join(', '))}`);
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If --docs is passed, auto-include 'docs' in modules
|
|
105
|
+
if (docsPath && !modules.includes('docs')) {
|
|
106
|
+
modules.push('docs');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Save config from TUI selection — only when TUI actually ran
|
|
110
|
+
// NEVER rewrite config on headless runs (existing config + no --setup)
|
|
111
|
+
if (tuiConfig) {
|
|
112
|
+
// New config (first run) — save everything
|
|
113
|
+
saveConfigFromTui(scan.repoPath, modules, tuiConfig.embedding, tuiConfig.pruner, tuiConfig.expander, tuiInclude, tuiIgnore);
|
|
114
|
+
} else if (tuiInclude.length > 0 || tuiIgnore.length > 0) {
|
|
115
|
+
// TUI ran with existing config and user changed selections — update patterns only
|
|
116
|
+
updateConfigPlugins(scan.repoPath, modules, tuiInclude, tuiIgnore);
|
|
117
|
+
}
|
|
118
|
+
// If neither condition is true, config already exists and TUI didn't run — don't touch it
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
console.log(c.bold(`\n━━━ Indexing: ${modules.join(', ')} ━━━`));
|
|
122
|
+
|
|
123
|
+
// Build brain context, injecting TUI-selected include/ignore patterns
|
|
124
|
+
const ctx = contextFromCLI(repoPath);
|
|
125
|
+
if (tuiInclude.length > 0 && !ctx.flags?.include) {
|
|
126
|
+
ctx.flags = { ...ctx.flags, include: tuiInclude.join(',') };
|
|
127
|
+
}
|
|
128
|
+
if (tuiIgnore.length > 0 && !ctx.flags?.ignore) {
|
|
129
|
+
ctx.flags = { ...ctx.flags, ignore: tuiIgnore.join(',') };
|
|
130
|
+
}
|
|
131
|
+
const brain = await createBrain(ctx);
|
|
132
|
+
await brain.initialize();
|
|
133
|
+
|
|
134
|
+
const config = await getConfig(repoPath);
|
|
135
|
+
await registerConfigCollections(brain, repoPath, config);
|
|
136
|
+
|
|
137
|
+
if (docsPath) {
|
|
138
|
+
const absDocsPath = path.resolve(docsPath);
|
|
139
|
+
const collName = path.basename(absDocsPath);
|
|
140
|
+
try {
|
|
141
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
142
|
+
await docsPlugin?.addCollection({
|
|
143
|
+
name: collName,
|
|
144
|
+
path: absDocsPath,
|
|
145
|
+
pattern: '**/*.md',
|
|
146
|
+
ignore: ['deprecated/**', 'node_modules/**'],
|
|
147
|
+
});
|
|
148
|
+
console.log(c.dim(` Registered docs collection: ${collName}`));
|
|
149
|
+
} catch {
|
|
150
|
+
console.log(c.yellow(` Warning: docs module not loaded, skipping --docs`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = await brain.index({
|
|
155
|
+
modules,
|
|
156
|
+
forceReindex: force,
|
|
157
|
+
pluginOptions: { depth },
|
|
158
|
+
onProgress: (stage, msg) => {
|
|
159
|
+
process.stdout.write(`\r ${c.cyan(stage.toUpperCase())} ${msg} `);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log('\n');
|
|
164
|
+
|
|
165
|
+
// ── Changes summary ──
|
|
166
|
+
console.log(c.bold('\n━━━ Changes ━━━\n'));
|
|
167
|
+
let hasChanges = false;
|
|
168
|
+
for (const [name, value] of Object.entries(result)) {
|
|
169
|
+
if (!value) continue;
|
|
170
|
+
const v = value as Record<string, unknown>;
|
|
171
|
+
if (typeof v.indexed !== 'number') { console.log(` ${c.green('✓')} ${name}: done`); continue; }
|
|
172
|
+
|
|
173
|
+
const indexed = v.indexed as number;
|
|
174
|
+
const skipped = (v.skipped ?? 0) as number;
|
|
175
|
+
const removed = (v.removed ?? 0) as number;
|
|
176
|
+
const chunks = (v.chunks ?? 0) as number;
|
|
177
|
+
|
|
178
|
+
if (indexed > 0 || removed > 0) hasChanges = true;
|
|
179
|
+
|
|
180
|
+
const parts: string[] = [];
|
|
181
|
+
if (indexed > 0) parts.push(c.green(`+${indexed} files (${chunks} chunks)`));
|
|
182
|
+
if (removed > 0) parts.push(c.red(`−${removed} files`));
|
|
183
|
+
if (skipped > 0) parts.push(c.dim(`${skipped} unchanged`));
|
|
184
|
+
|
|
185
|
+
console.log(` ${c.bold(name)}: ${parts.join(' ')}`);
|
|
186
|
+
}
|
|
187
|
+
if (!hasChanges) {
|
|
188
|
+
console.log(c.dim(' No changes — everything up to date'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stats = brain.stats();
|
|
192
|
+
console.log(`\n ${c.bold('Totals')}:`);
|
|
193
|
+
for (const [name, s] of Object.entries(stats)) {
|
|
194
|
+
if (!s || typeof s !== 'object') continue;
|
|
195
|
+
const entries = Object.entries(s as Record<string, unknown>)
|
|
196
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
197
|
+
.join(', ');
|
|
198
|
+
console.log(` ${name}: ${entries}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
brain.close();
|
|
202
|
+
|
|
203
|
+
// Auto-export MCP config to Antigravity if detected and not already configured
|
|
204
|
+
await autoExportMcp(repoPath);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/** Compact header for headless (non-TUI) index runs. Validates include/ignore paths. */
|
|
209
|
+
function printIndexHeader(scan: ScanResult, _depth: number): void {
|
|
210
|
+
console.log(c.bold('\n━━━ BrainBank ━━━'));
|
|
211
|
+
console.log(c.dim(` ${scan.repoPath}\n`));
|
|
212
|
+
|
|
213
|
+
// Show plugins
|
|
214
|
+
const plugins = scan.config.plugins ?? [];
|
|
215
|
+
console.log(` Plugins: ${c.cyan(plugins.join(', '))}`);
|
|
216
|
+
|
|
217
|
+
// Validate and show include patterns
|
|
218
|
+
if (scan.config.include?.length) {
|
|
219
|
+
console.log('');
|
|
220
|
+
for (const pattern of scan.config.include) {
|
|
221
|
+
const exists = validatePattern(scan.repoPath, pattern);
|
|
222
|
+
const icon = exists ? c.green('✓') : c.red('✗');
|
|
223
|
+
const label = exists ? c.dim(pattern) : c.red(pattern);
|
|
224
|
+
console.log(` ${icon} ${label}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Validate and show ignore patterns
|
|
229
|
+
if (scan.config.ignore?.length) {
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log(c.dim(' Ignore:'));
|
|
232
|
+
for (const pattern of scan.config.ignore) {
|
|
233
|
+
console.log(` ${c.yellow('─')} ${c.dim(pattern)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// DB info
|
|
238
|
+
if (scan.db?.exists) {
|
|
239
|
+
const ago = scan.db.lastModified ? timeSince(scan.db.lastModified) : '';
|
|
240
|
+
console.log(c.dim(`\n DB: ${scan.db.sizeMB} MB${ago ? `, last indexed ${ago}` : ''}`));
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Check if a glob pattern's base directory exists on disk. */
|
|
246
|
+
function validatePattern(repoPath: string, pattern: string): boolean {
|
|
247
|
+
// Strip trailing /** or /* or glob chars
|
|
248
|
+
const base = pattern.replace(/\/\*\*$/, '').replace(/\/\*$/, '');
|
|
249
|
+
const absPath = path.join(repoPath, base);
|
|
250
|
+
try {
|
|
251
|
+
fs.statSync(absPath);
|
|
252
|
+
return true;
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
/** Build the default list of available modules based on scan. */
|
|
260
|
+
function buildDefaultModules(scan: ScanResult): string[] {
|
|
261
|
+
return scan.modules.filter(m => m.available && m.checked).map(m => m.name);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
function timeSince(date: Date): string {
|
|
266
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
267
|
+
if (seconds < 60) return 'just now';
|
|
268
|
+
const minutes = Math.floor(seconds / 60);
|
|
269
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
270
|
+
const hours = Math.floor(minutes / 60);
|
|
271
|
+
if (hours < 24) return `${hours}h ago`;
|
|
272
|
+
const days = Math.floor(hours / 24);
|
|
273
|
+
return `${days}d ago`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Capitalize first letter. */
|
|
277
|
+
function capitalizeFirst(s: string): string {
|
|
278
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Common doc folder names to auto-detect for docs plugin. */
|
|
282
|
+
const DOC_FOLDERS = ['docs', 'doc', 'wiki', 'documentation', 'guides', 'notes'];
|
|
283
|
+
|
|
284
|
+
/** Auto-detect document collections in a repo. Scans for common doc folders + README. */
|
|
285
|
+
function autoDetectDocCollections(repoPath: string): { name: string; path: string; pattern: string; context?: string }[] {
|
|
286
|
+
const resolved = path.resolve(repoPath);
|
|
287
|
+
const collections: { name: string; path: string; pattern: string; context?: string }[] = [];
|
|
288
|
+
|
|
289
|
+
for (const folder of DOC_FOLDERS) {
|
|
290
|
+
const absPath = path.join(resolved, folder);
|
|
291
|
+
try {
|
|
292
|
+
const stat = fs.statSync(absPath);
|
|
293
|
+
if (stat.isDirectory()) {
|
|
294
|
+
// Check it actually contains markdown files
|
|
295
|
+
const entries = fs.readdirSync(absPath, { recursive: true }) as string[];
|
|
296
|
+
const hasMd = entries.some(e => typeof e === 'string' && /\.md$/i.test(e));
|
|
297
|
+
if (hasMd) {
|
|
298
|
+
collections.push({
|
|
299
|
+
name: folder,
|
|
300
|
+
path: folder,
|
|
301
|
+
pattern: '**/*.md',
|
|
302
|
+
context: `${folder} directory`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
// Folder doesn't exist — skip
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return collections;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Save config.json from TUI selections (no interactive prompts). */
|
|
315
|
+
function saveConfigFromTui(
|
|
316
|
+
repoPath: string, modules: string[], embedding: string, pruner: string, expander: string,
|
|
317
|
+
include: string[], ignore: string[],
|
|
318
|
+
): void {
|
|
319
|
+
const configDir = path.join(repoPath, '.brainbank');
|
|
320
|
+
const configPath = path.join(configDir, 'config.json');
|
|
321
|
+
|
|
322
|
+
const config: Record<string, unknown> = {
|
|
323
|
+
plugins: modules,
|
|
324
|
+
embedding,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (pruner !== 'none') {
|
|
328
|
+
config.pruner = pruner;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (expander !== 'none') {
|
|
332
|
+
config.expander = expander;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Save include/ignore from tree selection
|
|
336
|
+
if (include.length > 0) {
|
|
337
|
+
config.include = include;
|
|
338
|
+
}
|
|
339
|
+
if (ignore.length > 0) {
|
|
340
|
+
config.ignore = ignore;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Auto-detect doc collections when docs plugin is selected
|
|
344
|
+
if (modules.includes('docs')) {
|
|
345
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
346
|
+
if (collections.length > 0) {
|
|
347
|
+
config.docs = { collections };
|
|
348
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map(dc => dc.name).join(', ')}`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Auto-detect API keys from environment
|
|
353
|
+
const detectedKeys: Record<string, string> = {};
|
|
354
|
+
const needsPerplexity = embedding.startsWith('perplexity');
|
|
355
|
+
const needsAnthropic = pruner === 'haiku' || expander === 'haiku';
|
|
356
|
+
const needsOpenai = embedding === 'openai';
|
|
357
|
+
|
|
358
|
+
if (needsPerplexity && process.env.PERPLEXITY_API_KEY) {
|
|
359
|
+
detectedKeys.perplexity = process.env.PERPLEXITY_API_KEY;
|
|
360
|
+
}
|
|
361
|
+
if (needsAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
362
|
+
detectedKeys.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
363
|
+
}
|
|
364
|
+
if (needsOpenai && process.env.OPENAI_API_KEY) {
|
|
365
|
+
detectedKeys.openai = process.env.OPENAI_API_KEY;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (Object.keys(detectedKeys).length > 0) {
|
|
369
|
+
config.keys = detectedKeys;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
373
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
374
|
+
console.log(c.green(` ✓ Saved ${path.relative(process.cwd(), configPath)}`));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
/** Update plugins, include, and ignore in an existing config.json. */
|
|
379
|
+
function updateConfigPlugins(repoPath: string, modules: string[], include: string[], ignore: string[]): void {
|
|
380
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
381
|
+
try {
|
|
382
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
383
|
+
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
384
|
+
config.plugins = modules;
|
|
385
|
+
|
|
386
|
+
// Update include/ignore — set if present, remove if empty
|
|
387
|
+
if (include.length > 0) {
|
|
388
|
+
config.include = include;
|
|
389
|
+
} else {
|
|
390
|
+
delete config.include;
|
|
391
|
+
}
|
|
392
|
+
if (ignore.length > 0) {
|
|
393
|
+
config.ignore = ignore;
|
|
394
|
+
} else {
|
|
395
|
+
delete config.ignore;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Auto-detect doc collections when docs is newly added and none exist
|
|
399
|
+
if (modules.includes('docs')) {
|
|
400
|
+
const existing = config.docs as Record<string, unknown> | undefined;
|
|
401
|
+
const hasCollections = existing && Array.isArray(existing.collections) && existing.collections.length > 0;
|
|
402
|
+
if (!hasCollections) {
|
|
403
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
404
|
+
if (collections.length > 0) {
|
|
405
|
+
config.docs = { ...(existing ?? {}), collections };
|
|
406
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map(dc => dc.name).join(', ')}`));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
412
|
+
console.log(c.green(` ✓ Updated config.json`));
|
|
413
|
+
} catch {
|
|
414
|
+
// Config doesn't exist or is corrupt — skip
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Clear all indexed data for a specific module from the DB.
|
|
421
|
+
* Opens the SQLite database directly and drops module-specific rows.
|
|
422
|
+
*/
|
|
423
|
+
function deindexModule(repoPath: string, moduleName: string): void {
|
|
424
|
+
const dbPath = path.join(repoPath, '.brainbank', 'data', 'brainbank.db');
|
|
425
|
+
if (!fs.existsSync(dbPath)) return;
|
|
426
|
+
|
|
427
|
+
// Simple interface — we only need exec() and close()
|
|
428
|
+
interface SimpleDB { exec(sql: string): void; close(): void }
|
|
429
|
+
|
|
430
|
+
let db: SimpleDB | undefined;
|
|
431
|
+
try {
|
|
432
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
433
|
+
const sqlite = require('node:sqlite') as { DatabaseSync: new (path: string) => SimpleDB };
|
|
434
|
+
db = new sqlite.DatabaseSync(dbPath);
|
|
435
|
+
} catch {
|
|
436
|
+
console.log(c.yellow(` Could not open DB — skip deindex for ${moduleName}`));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!db) return;
|
|
441
|
+
|
|
442
|
+
const tables: Record<string, string[]> = {
|
|
443
|
+
code: [
|
|
444
|
+
'DELETE FROM code_call_edges',
|
|
445
|
+
'DELETE FROM code_refs',
|
|
446
|
+
'DELETE FROM code_symbols',
|
|
447
|
+
'DELETE FROM code_imports',
|
|
448
|
+
'DELETE FROM code_vectors',
|
|
449
|
+
'DELETE FROM code_chunks',
|
|
450
|
+
'DELETE FROM indexed_files',
|
|
451
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'code'",
|
|
452
|
+
],
|
|
453
|
+
docs: [
|
|
454
|
+
'DELETE FROM doc_vectors',
|
|
455
|
+
'DELETE FROM doc_chunks',
|
|
456
|
+
'DELETE FROM path_contexts',
|
|
457
|
+
'DELETE FROM collections',
|
|
458
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'docs'",
|
|
459
|
+
],
|
|
460
|
+
git: [
|
|
461
|
+
'DELETE FROM git_vectors',
|
|
462
|
+
'DELETE FROM git_commits',
|
|
463
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'git'",
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const statements = tables[moduleName];
|
|
468
|
+
if (!statements) {
|
|
469
|
+
console.log(c.dim(` No known tables for ${moduleName}`));
|
|
470
|
+
try { db.close(); } catch { /* ignore */ }
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const sql of statements) {
|
|
475
|
+
try { db.exec(sql); }
|
|
476
|
+
catch { /* Table might not exist — that's fine */ }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try { db.close(); } catch { /* ignore */ }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank kv add|search|list|trim|clear — Dynamic KV collection management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { c, args, getFlag, stripFlags } from '@/cli/utils.ts';
|
|
6
|
+
import { createBrain } from '@/cli/factory/index.ts';
|
|
7
|
+
|
|
8
|
+
export async function cmdKv(): Promise<void> {
|
|
9
|
+
const pos = stripFlags(args);
|
|
10
|
+
const sub = pos[1];
|
|
11
|
+
|
|
12
|
+
if (sub === 'add') {
|
|
13
|
+
const collName = pos[2];
|
|
14
|
+
const content = pos.slice(3).join(' ');
|
|
15
|
+
const metaRaw = getFlag('meta');
|
|
16
|
+
|
|
17
|
+
if (!collName || !content) {
|
|
18
|
+
console.log(c.red("Usage: brainbank kv add <collection> <content> [--meta '{\"key\":\"val\"}']"));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const brain = await createBrain();
|
|
23
|
+
await brain.initialize();
|
|
24
|
+
const coll = brain.collection(collName);
|
|
25
|
+
const meta = metaRaw ? JSON.parse(metaRaw) : {};
|
|
26
|
+
const id = await coll.add(content, meta);
|
|
27
|
+
console.log(c.green(`✓ Added item #${id} to '${collName}'`));
|
|
28
|
+
brain.close();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sub === 'search') {
|
|
33
|
+
const collName = pos[2];
|
|
34
|
+
const query = pos.slice(3).join(' ');
|
|
35
|
+
const k = parseInt(getFlag('k') || '5', 10);
|
|
36
|
+
const mode = (getFlag('mode') || 'hybrid') as 'hybrid' | 'vector' | 'keyword';
|
|
37
|
+
|
|
38
|
+
if (!collName || !query) {
|
|
39
|
+
console.log(c.red('Usage: brainbank kv search <collection> <query> [--k 5] [--mode hybrid|keyword|vector]'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const brain = await createBrain();
|
|
44
|
+
await brain.initialize();
|
|
45
|
+
const coll = brain.collection(collName);
|
|
46
|
+
const results = await coll.search(query, { k, mode });
|
|
47
|
+
|
|
48
|
+
if (results.length === 0) {
|
|
49
|
+
console.log(c.yellow(' No results found.'));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(c.bold(`\n━━━ ${collName}: "${query}" ━━━\n`));
|
|
52
|
+
for (const r of results) {
|
|
53
|
+
const score = Math.round((r.score ?? 0) * 100);
|
|
54
|
+
console.log(` ${c.cyan(`[${score}%]`)} ${r.content}`);
|
|
55
|
+
if (Object.keys(r.metadata).length > 0) {
|
|
56
|
+
console.log(` ${c.dim(JSON.stringify(r.metadata))}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
brain.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (sub === 'list') {
|
|
65
|
+
const collName = pos[2];
|
|
66
|
+
const limit = parseInt(getFlag('limit') || '20', 10);
|
|
67
|
+
|
|
68
|
+
if (!collName) {
|
|
69
|
+
const brain = await createBrain();
|
|
70
|
+
await brain.initialize();
|
|
71
|
+
const names = brain.listCollectionNames();
|
|
72
|
+
if (names.length === 0) {
|
|
73
|
+
console.log(c.yellow(' No KV collections found.'));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(c.bold('\n━━━ KV Collections ━━━\n'));
|
|
76
|
+
for (const n of names) {
|
|
77
|
+
const coll = brain.collection(n);
|
|
78
|
+
console.log(` ${c.cyan(n)} — ${coll.count()} items`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
brain.close();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const brain = await createBrain();
|
|
86
|
+
await brain.initialize();
|
|
87
|
+
const coll = brain.collection(collName);
|
|
88
|
+
const items = coll.list({ limit });
|
|
89
|
+
if (items.length === 0) {
|
|
90
|
+
console.log(c.yellow(` Collection '${collName}' is empty.`));
|
|
91
|
+
} else {
|
|
92
|
+
console.log(c.bold(`\n━━━ ${collName} (${coll.count()} items) ━━━\n`));
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
const age = Math.round((Date.now() / 1000 - item.createdAt) / 60);
|
|
95
|
+
console.log(` #${item.id} ${c.dim(`(${age}m ago)`)} ${item.content.slice(0, 80)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
brain.close();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (sub === 'trim') {
|
|
103
|
+
const collName = pos[2];
|
|
104
|
+
const keep = parseInt(getFlag('keep') || '0', 10);
|
|
105
|
+
|
|
106
|
+
if (!collName || keep <= 0) {
|
|
107
|
+
console.log(c.red('Usage: brainbank kv trim <collection> --keep <n>'));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const brain = await createBrain();
|
|
112
|
+
await brain.initialize();
|
|
113
|
+
const coll = brain.collection(collName);
|
|
114
|
+
const result = await coll.trim({ keep });
|
|
115
|
+
console.log(c.green(`✓ Trimmed ${result.removed} items from '${collName}' (kept ${keep})`));
|
|
116
|
+
brain.close();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (sub === 'clear') {
|
|
121
|
+
const collName = pos[2];
|
|
122
|
+
if (!collName) {
|
|
123
|
+
console.log(c.red('Usage: brainbank kv clear <collection>'));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const brain = await createBrain();
|
|
128
|
+
await brain.initialize();
|
|
129
|
+
const coll = brain.collection(collName);
|
|
130
|
+
const before = coll.count();
|
|
131
|
+
coll.clear();
|
|
132
|
+
console.log(c.green(`✓ Cleared ${before} items from '${collName}'`));
|
|
133
|
+
brain.close();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(c.red('Usage: brainbank kv <add|search|list|trim|clear>'));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|