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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank mcp:export [target] — Export MCP server config for AI IDEs.
|
|
3
|
+
*
|
|
4
|
+
* Generates the MCP server config block for brainbank and merges it into
|
|
5
|
+
* the target IDE's config file. Currently supports: antigravity.
|
|
6
|
+
*
|
|
7
|
+
* Detects: node path, cli.js path, API keys from config or env vars.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ProjectConfig } from '@/cli/factory/config-loader.ts';
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { c, args, getFlag } from '@/cli/utils.ts';
|
|
16
|
+
import { getConfig } from '@/cli/factory/index.ts';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/** Supported export targets and their config file paths. */
|
|
22
|
+
const TARGETS: Record<string, { configPath: string; label: string }> = {
|
|
23
|
+
antigravity: {
|
|
24
|
+
configPath: path.join(process.env.HOME ?? '~', '.gemini', 'antigravity', 'mcp_config.json'),
|
|
25
|
+
label: 'Gemini Antigravity',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface McpServerConfig {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
env?: Record<string, string>;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface McpConfig {
|
|
37
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the brainbank MCP server config block.
|
|
42
|
+
* Resolves node binary, dist/cli.js path, and API keys.
|
|
43
|
+
*/
|
|
44
|
+
function buildBrainbankMcpBlock(config: ProjectConfig | null): McpServerConfig {
|
|
45
|
+
const nodeBin = process.execPath;
|
|
46
|
+
|
|
47
|
+
// Resolve dist/cli.js from the global install location (node_prefix/lib/node_modules/brainbank/dist/cli.js)
|
|
48
|
+
const globalCliJs = path.join(path.dirname(nodeBin), '..', 'lib', 'node_modules', 'brainbank', 'dist', 'cli.js');
|
|
49
|
+
// Fallback: relative to this file (dev / npm link)
|
|
50
|
+
const localCliJs = path.resolve(__dirname, '..', '..', 'dist', 'cli.js');
|
|
51
|
+
const resolvedCliJs = fs.existsSync(globalCliJs) ? globalCliJs : localCliJs;
|
|
52
|
+
|
|
53
|
+
const env: Record<string, string> = {};
|
|
54
|
+
|
|
55
|
+
// Resolve API keys: config.keys > env vars
|
|
56
|
+
const keys = config?.keys;
|
|
57
|
+
const perplexityKey = keys?.perplexity ?? process.env.PERPLEXITY_API_KEY;
|
|
58
|
+
const anthropicKey = keys?.anthropic ?? process.env.ANTHROPIC_API_KEY;
|
|
59
|
+
const openaiKey = keys?.openai ?? process.env.OPENAI_API_KEY;
|
|
60
|
+
|
|
61
|
+
if (perplexityKey) env.PERPLEXITY_API_KEY = perplexityKey;
|
|
62
|
+
if (anthropicKey) env.ANTHROPIC_API_KEY = anthropicKey;
|
|
63
|
+
if (openaiKey) env.OPENAI_API_KEY = openaiKey;
|
|
64
|
+
|
|
65
|
+
const block: McpServerConfig = {
|
|
66
|
+
command: nodeBin,
|
|
67
|
+
args: ['--disable-warning=ExperimentalWarning', resolvedCliJs, 'mcp'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (Object.keys(env).length > 0) {
|
|
71
|
+
block.env = env;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return block;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load existing MCP config, merge brainbank entry, and write back.
|
|
79
|
+
* Preserves all other server entries.
|
|
80
|
+
*/
|
|
81
|
+
function mergeAndWrite(targetPath: string, block: McpServerConfig): { created: boolean } {
|
|
82
|
+
let existing: McpConfig = { mcpServers: {} };
|
|
83
|
+
const created = !fs.existsSync(targetPath);
|
|
84
|
+
|
|
85
|
+
if (!created) {
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(targetPath, 'utf-8');
|
|
88
|
+
existing = JSON.parse(raw) as McpConfig;
|
|
89
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
90
|
+
} catch {
|
|
91
|
+
// Corrupt or empty file — start fresh
|
|
92
|
+
existing = { mcpServers: {} };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
existing.mcpServers.brainbank = block;
|
|
97
|
+
|
|
98
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
99
|
+
fs.writeFileSync(targetPath, JSON.stringify(existing, null, 2) + '\n');
|
|
100
|
+
|
|
101
|
+
return { created };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Check if an MCP config already has a brainbank entry. */
|
|
105
|
+
export function hasBrainbankMcpEntry(targetPath: string): boolean {
|
|
106
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
107
|
+
try {
|
|
108
|
+
const raw = fs.readFileSync(targetPath, 'utf-8');
|
|
109
|
+
const config = JSON.parse(raw) as McpConfig;
|
|
110
|
+
return !!config.mcpServers?.brainbank;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Auto-export: called after index when Antigravity is detected. */
|
|
117
|
+
export async function autoExportMcp(repoPath: string): Promise<void> {
|
|
118
|
+
const target = TARGETS.antigravity;
|
|
119
|
+
if (!target) return;
|
|
120
|
+
|
|
121
|
+
// Only auto-export if Antigravity dir exists
|
|
122
|
+
const antigravityDir = path.dirname(target.configPath);
|
|
123
|
+
if (!fs.existsSync(antigravityDir)) return;
|
|
124
|
+
|
|
125
|
+
// Only auto-export if brainbank isn't already configured
|
|
126
|
+
if (hasBrainbankMcpEntry(target.configPath)) return;
|
|
127
|
+
|
|
128
|
+
const config = await getConfig(repoPath);
|
|
129
|
+
const block = buildBrainbankMcpBlock(config);
|
|
130
|
+
mergeAndWrite(target.configPath, block);
|
|
131
|
+
console.log(` ${c.green('✓')} Exported MCP config to ${c.dim(path.relative(process.env.HOME ?? '', target.configPath))}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── GEMINI.md BrainBank Section ──────────────────────────────
|
|
135
|
+
|
|
136
|
+
const BRAINBANK_SECTION_MARKER = '<!-- brainbank:start -->';
|
|
137
|
+
const BRAINBANK_SECTION_END = '<!-- brainbank:end -->';
|
|
138
|
+
|
|
139
|
+
/** Global GEMINI.md path — ~/.gemini/GEMINI.md (shared across all projects). */
|
|
140
|
+
const GLOBAL_GEMINI = path.join(process.env.HOME ?? '~', '.gemini', 'GEMINI.md');
|
|
141
|
+
|
|
142
|
+
function buildGeminiSection(): string {
|
|
143
|
+
return `${BRAINBANK_SECTION_MARKER}
|
|
144
|
+
|
|
145
|
+
## BrainBank — Code Intelligence
|
|
146
|
+
|
|
147
|
+
**\`brainbank_context\`** — semantic code discovery. Use it **only** when you need to understand functionality that spans multiple files (e.g. "how does auth work", "notification pipeline"). \`repo\` is **required**.
|
|
148
|
+
|
|
149
|
+
**Do NOT use it for:** finding a specific string, variable, function name, or exact match — use \`grep_search\` for that. BrainBank is for *understanding context*, not for *locating text*.
|
|
150
|
+
|
|
151
|
+
**Params:** \`compact: false\`, \`sources: { code: 25, docs: 0, git: 0 }\`. Only add \`docs\` when the user explicitly asks about documentation.
|
|
152
|
+
|
|
153
|
+
### Output handling — MANDATORY
|
|
154
|
+
|
|
155
|
+
BrainBank may return results **inline** or save them to an **output file**:
|
|
156
|
+
|
|
157
|
+
- **Inline:** read and proceed normally.
|
|
158
|
+
- **File result** (saved to \`output.txt\`): you **MUST STOP IMMEDIATELY**. Show the path as a clickable link, ask the user to paste, then **DO NOT PROCEED**:
|
|
159
|
+
|
|
160
|
+
📋 Output: [output.txt](file:///path/to/output.txt) — _"Could you paste the context output here?"_
|
|
161
|
+
|
|
162
|
+
**NEVER** read the file yourself. **NEVER** continue working. Wait for the user to paste the content. This is not optional.
|
|
163
|
+
|
|
164
|
+
${BRAINBANK_SECTION_END}
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Check if GEMINI.md already has the brainbank section. */
|
|
169
|
+
function hasGeminiSection(geminiPath: string): boolean {
|
|
170
|
+
if (!fs.existsSync(geminiPath)) return false;
|
|
171
|
+
const content = fs.readFileSync(geminiPath, 'utf-8');
|
|
172
|
+
return content.includes(BRAINBANK_SECTION_MARKER);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Append BrainBank section to GEMINI.md (creates if doesn't exist). */
|
|
176
|
+
function appendGeminiSection(geminiPath: string): void {
|
|
177
|
+
const section = buildGeminiSection();
|
|
178
|
+
if (fs.existsSync(geminiPath)) {
|
|
179
|
+
fs.appendFileSync(geminiPath, section);
|
|
180
|
+
} else {
|
|
181
|
+
fs.writeFileSync(geminiPath, `# GEMINI.md\n${section}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Replace BrainBank section between markers in GEMINI.md. */
|
|
186
|
+
function replaceGeminiSection(geminiPath: string): void {
|
|
187
|
+
const content = fs.readFileSync(geminiPath, 'utf-8');
|
|
188
|
+
const startIdx = content.indexOf(BRAINBANK_SECTION_MARKER);
|
|
189
|
+
const endIdx = content.indexOf(BRAINBANK_SECTION_END);
|
|
190
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
191
|
+
|
|
192
|
+
const before = content.slice(0, startIdx);
|
|
193
|
+
const after = content.slice(endIdx + BRAINBANK_SECTION_END.length);
|
|
194
|
+
const section = buildGeminiSection();
|
|
195
|
+
fs.writeFileSync(geminiPath, before + section + after);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** CLI command: brainbank mcp:export [target] [--force] */
|
|
199
|
+
export async function cmdMcpExport(): Promise<void> {
|
|
200
|
+
const targetName = args[1] || getFlag('target') || 'antigravity';
|
|
201
|
+
const repoPath = getFlag('repo') || '.';
|
|
202
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
203
|
+
|
|
204
|
+
const target = TARGETS[targetName];
|
|
205
|
+
if (!target) {
|
|
206
|
+
console.error(c.red(`Unknown export target: ${targetName}`));
|
|
207
|
+
console.error(c.dim(` Available: ${Object.keys(TARGETS).join(', ')}`));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const config = await getConfig(repoPath);
|
|
212
|
+
const block = buildBrainbankMcpBlock(config);
|
|
213
|
+
|
|
214
|
+
console.log(c.bold(`\n━━━ MCP Export: ${target.label} ━━━\n`));
|
|
215
|
+
|
|
216
|
+
// ── MCP Config ────────────────────────────────────────────
|
|
217
|
+
const mcpExists = hasBrainbankMcpEntry(target.configPath);
|
|
218
|
+
let writeMcp = true;
|
|
219
|
+
|
|
220
|
+
if (mcpExists && !force) {
|
|
221
|
+
console.log(` ${c.yellow('●')} MCP config already has brainbank entry`);
|
|
222
|
+
const cliPath = block.args.find(a => !a.startsWith('--')) ?? block.args[0];
|
|
223
|
+
console.log(` ${c.dim(' New:')} ${block.command} ${cliPath}`);
|
|
224
|
+
const envKeys = block.env ? Object.keys(block.env) : [];
|
|
225
|
+
if (envKeys.length > 0) console.log(` ${c.dim(' Keys:')} ${envKeys.join(', ')}`);
|
|
226
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
227
|
+
writeMcp = await confirm({ message: 'Override existing brainbank MCP entry?', default: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (writeMcp) {
|
|
231
|
+
const { created } = mergeAndWrite(target.configPath, block);
|
|
232
|
+
console.log(` ${c.green('✓')} ${created ? 'Created' : 'Updated'} ${c.dim(target.configPath)}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.log(` ${c.dim('MCP config — skipped')}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Global GEMINI.md (~/.gemini/GEMINI.md) ────────────────
|
|
238
|
+
const geminiHasSection = hasGeminiSection(GLOBAL_GEMINI);
|
|
239
|
+
|
|
240
|
+
if (geminiHasSection) {
|
|
241
|
+
if (force) {
|
|
242
|
+
replaceGeminiSection(GLOBAL_GEMINI);
|
|
243
|
+
console.log(` ${c.green('✓')} Replaced BrainBank section in ${c.dim('~/.gemini/GEMINI.md')}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(` ${c.yellow('●')} ~/.gemini/GEMINI.md already has BrainBank section`);
|
|
246
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
247
|
+
const override = await confirm({ message: 'Override existing BrainBank section?', default: false });
|
|
248
|
+
if (override) {
|
|
249
|
+
replaceGeminiSection(GLOBAL_GEMINI);
|
|
250
|
+
console.log(` ${c.green('✓')} Replaced BrainBank section in ${c.dim('~/.gemini/GEMINI.md')}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(` ${c.dim('GEMINI.md — skipped')}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
if (force) {
|
|
257
|
+
appendGeminiSection(GLOBAL_GEMINI);
|
|
258
|
+
console.log(` ${c.green('✓')} Added BrainBank section to ${c.dim('~/.gemini/GEMINI.md')}`);
|
|
259
|
+
} else {
|
|
260
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
261
|
+
const addGemini = await confirm({
|
|
262
|
+
message: 'Add BrainBank instructions to ~/.gemini/GEMINI.md? (teaches AI tools how to use BrainBank)',
|
|
263
|
+
default: true,
|
|
264
|
+
});
|
|
265
|
+
if (addGemini) {
|
|
266
|
+
appendGeminiSection(GLOBAL_GEMINI);
|
|
267
|
+
console.log(` ${c.green('✓')} Added BrainBank section to ${c.dim('~/.gemini/GEMINI.md')}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`\n ${c.dim('Restart your IDE to apply changes.')}\n`);
|
|
273
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** brainbank reembed — Re-embed all vectors. */
|
|
2
|
+
|
|
3
|
+
import { c } from '@/cli/utils.ts';
|
|
4
|
+
import { createBrain } from '@/cli/factory/index.ts';
|
|
5
|
+
|
|
6
|
+
export async function cmdReembed(): Promise<void> {
|
|
7
|
+
const brain = await createBrain();
|
|
8
|
+
await brain.initialize();
|
|
9
|
+
|
|
10
|
+
console.log(c.bold('\n━━━ BrainBank Re-embed ━━━\n'));
|
|
11
|
+
console.log(c.dim(' Regenerating vectors with current embedding provider...'));
|
|
12
|
+
console.log(c.dim(' Text, FTS, and metadata remain unchanged.\n'));
|
|
13
|
+
|
|
14
|
+
const result = await brain.reembed({
|
|
15
|
+
onProgress: (table: string, current: number, total: number) => {
|
|
16
|
+
process.stdout.write(`\r ${c.cyan(table.padEnd(8))} ${current}/${total}`);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log('\n');
|
|
21
|
+
for (const [name, count] of Object.entries(result.counts)) {
|
|
22
|
+
if (count > 0) {
|
|
23
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
24
|
+
console.log(` ${c.green('✓')} ${label.padEnd(8)} ${count} vectors`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.log(`\n ${c.bold('Total')}: ${result.total} vectors regenerated\n`);
|
|
28
|
+
|
|
29
|
+
brain.close();
|
|
30
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank scan — Lightweight repo scanner for the interactive index flow.
|
|
3
|
+
*
|
|
4
|
+
* Scans the filesystem WITHOUT initializing BrainBank. Returns a ScanResult
|
|
5
|
+
* describing what's available to index via dynamic ScanModule descriptors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import picomatch from 'picomatch';
|
|
12
|
+
import { SUPPORTED_EXTENSIONS, isIgnoredDir, isIgnoredFile } from '@/lib/languages.ts';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** A single scannable module (plugin). */
|
|
16
|
+
export interface ScanModule {
|
|
17
|
+
/** Plugin name (e.g. 'code', 'git', 'docs'). */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Whether there's content available to index. */
|
|
20
|
+
available: boolean;
|
|
21
|
+
/** Human-readable summary (e.g. '1243 files (5 languages)'). */
|
|
22
|
+
summary: string;
|
|
23
|
+
/** Emoji icon for display. */
|
|
24
|
+
icon: string;
|
|
25
|
+
/** Whether checked by default in the prompt. */
|
|
26
|
+
checked: boolean;
|
|
27
|
+
/** Reason this module is disabled (shown in prompt). */
|
|
28
|
+
disabled?: string;
|
|
29
|
+
/** Detail lines for the scan tree (e.g. per-language breakdown). */
|
|
30
|
+
details?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScanResult {
|
|
34
|
+
repoPath: string;
|
|
35
|
+
modules: ScanModule[];
|
|
36
|
+
config: { exists: boolean; ignore?: string[]; include?: string[]; plugins?: string[] };
|
|
37
|
+
db: { exists: boolean; sizeMB: number; lastModified?: Date } | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/** Scan a repo path and return what's available to index. */
|
|
42
|
+
export function scanRepo(repoPath: string): ScanResult {
|
|
43
|
+
const resolved = path.resolve(repoPath);
|
|
44
|
+
const config = scanConfig(resolved);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
repoPath: resolved,
|
|
48
|
+
modules: scanModules(resolved, config),
|
|
49
|
+
config,
|
|
50
|
+
db: scanDb(resolved),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Produce ScanModule descriptors for known plugin types. */
|
|
55
|
+
function scanModules(repoPath: string, config: ScanResult['config']): ScanModule[] {
|
|
56
|
+
return [
|
|
57
|
+
scanCodeModule(repoPath, config.include, config.ignore),
|
|
58
|
+
scanGitModule(repoPath),
|
|
59
|
+
scanDocsModule(repoPath),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/** Scan for indexable code files. Respects include/ignore from config. */
|
|
65
|
+
function scanCodeModule(repoPath: string, include?: string[], ignore?: string[]): ScanModule {
|
|
66
|
+
const byLanguage = new Map<string, number>();
|
|
67
|
+
let total = 0;
|
|
68
|
+
|
|
69
|
+
// Build matchers from config patterns
|
|
70
|
+
let isIncluded: ((p: string) => boolean) | null = null;
|
|
71
|
+
let isIgnoredPat: ((p: string) => boolean) | null = null;
|
|
72
|
+
let includeBases: string[] | null = null;
|
|
73
|
+
if (include?.length) {
|
|
74
|
+
isIncluded = picomatch(include, { dot: true });
|
|
75
|
+
includeBases = include.map(p => picomatch.scan(p).base).filter(b => b && b !== '.');
|
|
76
|
+
}
|
|
77
|
+
if (ignore?.length) isIgnoredPat = picomatch(ignore, { dot: true });
|
|
78
|
+
|
|
79
|
+
function walk(dir: string): void {
|
|
80
|
+
let entries: fs.Dirent[];
|
|
81
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
82
|
+
catch { return; }
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(dir, entry.name);
|
|
86
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isDirectory(); } catch { return false; } })());
|
|
87
|
+
if (isDir) {
|
|
88
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
89
|
+
// Early prune: if include bases are set, skip dirs that can't match
|
|
90
|
+
if (includeBases && includeBases.length > 0) {
|
|
91
|
+
const relDir = path.relative(repoPath, fullPath);
|
|
92
|
+
const canMatch = includeBases.some(base =>
|
|
93
|
+
relDir.startsWith(base) || base.startsWith(relDir),
|
|
94
|
+
);
|
|
95
|
+
if (!canMatch) continue;
|
|
96
|
+
}
|
|
97
|
+
walk(fullPath);
|
|
98
|
+
} else if (entry.isFile()) {
|
|
99
|
+
if (isIgnoredFile(entry.name)) continue;
|
|
100
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
101
|
+
const lang = SUPPORTED_EXTENSIONS[ext];
|
|
102
|
+
if (!lang) continue;
|
|
103
|
+
|
|
104
|
+
// Apply include/ignore filters using relative path
|
|
105
|
+
const rel = path.relative(repoPath, fullPath);
|
|
106
|
+
if (isIncluded && !isIncluded(rel)) continue;
|
|
107
|
+
if (isIgnoredPat && isIgnoredPat(rel)) continue;
|
|
108
|
+
|
|
109
|
+
byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
|
|
110
|
+
total++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walk(repoPath);
|
|
116
|
+
|
|
117
|
+
if (total === 0) {
|
|
118
|
+
return { name: 'code', available: false, summary: 'no supported source files found', icon: '📁', checked: false, disabled: 'nothing to index' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const langCount = byLanguage.size;
|
|
122
|
+
const sorted = [...byLanguage.entries()].sort((a, b) => b[1] - a[1]);
|
|
123
|
+
const maxShow = 7;
|
|
124
|
+
const shown = sorted.slice(0, maxShow);
|
|
125
|
+
const remaining = sorted.length - maxShow;
|
|
126
|
+
|
|
127
|
+
const details: string[] = [];
|
|
128
|
+
for (let i = 0; i < shown.length; i++) {
|
|
129
|
+
const [lang, count] = shown[i];
|
|
130
|
+
const isLast = i === shown.length - 1 && remaining <= 0;
|
|
131
|
+
const prefix = isLast ? '└──' : '├──';
|
|
132
|
+
details.push(`${prefix} ${lang.padEnd(14)} ${count} files`);
|
|
133
|
+
}
|
|
134
|
+
if (remaining > 0) {
|
|
135
|
+
details.push(`└── ...and ${remaining} more`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name: 'code',
|
|
140
|
+
available: true,
|
|
141
|
+
summary: `${total} files (${langCount} language${langCount > 1 ? 's' : ''})`,
|
|
142
|
+
icon: '📁',
|
|
143
|
+
checked: true,
|
|
144
|
+
details,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Scan for git history. */
|
|
149
|
+
function scanGitModule(repoPath: string): ScanModule {
|
|
150
|
+
const stats = scanGitStats(repoPath);
|
|
151
|
+
|
|
152
|
+
if (!stats) {
|
|
153
|
+
return { name: 'git', available: false, summary: 'no .git directory found', icon: '📜', checked: false, disabled: 'not a git repo' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const details: string[] = [];
|
|
157
|
+
if (stats.lastMessage) {
|
|
158
|
+
details.push(`Last: ${stats.lastMessage} (${stats.lastDate})`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
name: 'git',
|
|
163
|
+
available: true,
|
|
164
|
+
summary: `${stats.commitCount.toLocaleString()} commits`,
|
|
165
|
+
icon: '📜',
|
|
166
|
+
checked: true,
|
|
167
|
+
details,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Scan for document collections. */
|
|
172
|
+
function scanDocsModule(repoPath: string): ScanModule {
|
|
173
|
+
const collections = scanDocsCollections(repoPath);
|
|
174
|
+
|
|
175
|
+
if (collections.length === 0) {
|
|
176
|
+
return { name: 'docs', available: false, summary: 'no documents found', icon: '📄', checked: false, disabled: 'no .md/.mdx files' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalFiles = collections.reduce((s, d) => s + d.fileCount, 0);
|
|
180
|
+
const details = collections.map((d, i) => {
|
|
181
|
+
const isLast = i === collections.length - 1;
|
|
182
|
+
const prefix = isLast ? '└──' : '├──';
|
|
183
|
+
return `${prefix} ${d.name.padEnd(10)} → ${d.path} (${d.fileCount} files)`;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name: 'docs',
|
|
188
|
+
available: true,
|
|
189
|
+
summary: `${collections.length} collection${collections.length > 1 ? 's' : ''} (${totalFiles} files)`,
|
|
190
|
+
icon: '📄',
|
|
191
|
+
checked: true,
|
|
192
|
+
details,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/** Get git stats for this repo. */
|
|
198
|
+
function scanGitStats(repoPath: string): { commitCount: number; lastMessage: string; lastDate: string } | null {
|
|
199
|
+
if (!fs.existsSync(path.join(repoPath, '.git'))) return null;
|
|
200
|
+
return gitStats(repoPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Get git stats for a single directory. */
|
|
204
|
+
function gitStats(dir: string): { commitCount: number; lastMessage: string; lastDate: string } | null {
|
|
205
|
+
try {
|
|
206
|
+
const count = parseInt(execSync('git rev-list --count HEAD', { cwd: dir, encoding: 'utf-8' }).trim(), 10);
|
|
207
|
+
const log = execSync('git log -1 --format="%s|%ar"', { cwd: dir, encoding: 'utf-8' }).trim();
|
|
208
|
+
const [lastMessage, lastDate] = log.split('|');
|
|
209
|
+
return { commitCount: count, lastMessage: lastMessage ?? '', lastDate: lastDate ?? '' };
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Scan for document collections (config + auto-detect). */
|
|
216
|
+
function scanDocsCollections(repoPath: string): { name: string; path: string; fileCount: number }[] {
|
|
217
|
+
const results: { name: string; path: string; fileCount: number }[] = [];
|
|
218
|
+
const seen = new Set<string>();
|
|
219
|
+
|
|
220
|
+
// 1. Read explicit collections from config.json
|
|
221
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
222
|
+
try {
|
|
223
|
+
if (fs.existsSync(configPath)) {
|
|
224
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
225
|
+
const docsCfg = config?.docs as Record<string, unknown> | undefined;
|
|
226
|
+
const collections = docsCfg?.collections as { name: string; path: string }[] | undefined;
|
|
227
|
+
if (collections) {
|
|
228
|
+
for (const coll of collections) {
|
|
229
|
+
const absPath = path.resolve(repoPath, coll.path);
|
|
230
|
+
results.push({ name: coll.name, path: coll.path, fileCount: countDocs(absPath) });
|
|
231
|
+
seen.add(absPath);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
|
|
237
|
+
// 2. Auto-detect .md/.mdx in the repo root and top-level dirs
|
|
238
|
+
const rootDocs = countDocsShallow(repoPath);
|
|
239
|
+
if (rootDocs > 0) {
|
|
240
|
+
results.push({ name: '(root)', path: '.', fileCount: rootDocs });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
for (const entry of fs.readdirSync(repoPath, { withFileTypes: true })) {
|
|
245
|
+
if (!entry.isDirectory()) continue;
|
|
246
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
247
|
+
if (entry.name.startsWith('.')) continue;
|
|
248
|
+
|
|
249
|
+
const dirPath = path.join(repoPath, entry.name);
|
|
250
|
+
if (seen.has(dirPath)) continue;
|
|
251
|
+
|
|
252
|
+
const count = countDocs(dirPath);
|
|
253
|
+
if (count > 0) {
|
|
254
|
+
results.push({ name: entry.name, path: `./${entry.name}`, fileCount: count });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Count .md/.mdx files recursively in a directory. */
|
|
263
|
+
function countDocs(dir: string): number {
|
|
264
|
+
let count = 0;
|
|
265
|
+
try {
|
|
266
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
267
|
+
const ePath = path.join(dir, e.name);
|
|
268
|
+
const isDir = e.isDirectory() || (e.isSymbolicLink() && (() => { try { return fs.statSync(ePath).isDirectory(); } catch { return false; } })());
|
|
269
|
+
if (isDir) {
|
|
270
|
+
if (isIgnoredDir(e.name)) continue;
|
|
271
|
+
count += countDocs(ePath);
|
|
272
|
+
} else if ((e.isFile() || e.isSymbolicLink()) && /\.mdx?$/i.test(e.name)) {
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Count .md/.mdx files in a directory (non-recursive, root level only). */
|
|
281
|
+
function countDocsShallow(dir: string): number {
|
|
282
|
+
let count = 0;
|
|
283
|
+
try {
|
|
284
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
285
|
+
if (e.isFile() && /\.mdx?$/i.test(e.name)) count++;
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
return count;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Check if config.json exists and read key fields. */
|
|
292
|
+
function scanConfig(repoPath: string): ScanResult['config'] {
|
|
293
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
294
|
+
if (!fs.existsSync(configPath)) return { exists: false };
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
298
|
+
const codeCfg = config?.code as Record<string, unknown> | undefined;
|
|
299
|
+
|
|
300
|
+
// Merge root-level and per-plugin include/ignore
|
|
301
|
+
const rootInclude = config?.include as string[] | undefined;
|
|
302
|
+
const rootIgnore = config?.ignore as string[] | undefined;
|
|
303
|
+
const pluginInclude = codeCfg?.include as string[] | undefined;
|
|
304
|
+
const pluginIgnore = codeCfg?.ignore as string[] | undefined;
|
|
305
|
+
|
|
306
|
+
const include = [...(rootInclude ?? []), ...(pluginInclude ?? [])];
|
|
307
|
+
const ignore = [...(rootIgnore ?? []), ...(pluginIgnore ?? [])];
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
exists: true,
|
|
311
|
+
ignore: ignore.length > 0 ? ignore : undefined,
|
|
312
|
+
include: include.length > 0 ? include : undefined,
|
|
313
|
+
plugins: config?.plugins as string[] | undefined,
|
|
314
|
+
};
|
|
315
|
+
} catch {
|
|
316
|
+
return { exists: false };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Check DB existence and size. */
|
|
321
|
+
function scanDb(repoPath: string): ScanResult['db'] {
|
|
322
|
+
const dbPath = path.join(repoPath, '.brainbank', 'data', 'brainbank.db');
|
|
323
|
+
if (!fs.existsSync(dbPath)) return { exists: false, sizeMB: 0 };
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const stat = fs.statSync(dbPath);
|
|
327
|
+
return {
|
|
328
|
+
exists: true,
|
|
329
|
+
sizeMB: Math.round(stat.size / 1024 / 1024 * 10) / 10,
|
|
330
|
+
lastModified: stat.mtime,
|
|
331
|
+
};
|
|
332
|
+
} catch {
|
|
333
|
+
return { exists: false, sizeMB: 0 };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|