brainbank 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/assets/architecture.png +0 -0
- package/bin/brainbank +18 -0
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-63GBCDS5.js +3249 -0
- package/dist/chunk-63GBCDS5.js.map +1 -0
- package/dist/chunk-DMFMTOHF.js +123 -0
- package/dist/chunk-DMFMTOHF.js.map +1 -0
- package/dist/chunk-FQYKWB2Q.js +136 -0
- package/dist/chunk-FQYKWB2Q.js.map +1 -0
- package/dist/chunk-IMJJ2VEM.js +74 -0
- package/dist/chunk-IMJJ2VEM.js.map +1 -0
- package/dist/chunk-M744PCJQ.js +43 -0
- package/dist/chunk-M744PCJQ.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-OPH7GZ7U.js +124 -0
- package/dist/chunk-OPH7GZ7U.js.map +1 -0
- package/dist/chunk-PXEWQMN7.js +89 -0
- package/dist/chunk-PXEWQMN7.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-VIIHPCC4.js +254 -0
- package/dist/chunk-VIIHPCC4.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3076 -0
- package/dist/cli.js.map +1 -0
- package/dist/haiku-expander-YRSIPGKP.js +8 -0
- package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
- package/dist/haiku-pruner-SHAXUPY6.js +8 -0
- package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
- package/dist/http-server-QUXHLWUM.js +9 -0
- package/dist/http-server-QUXHLWUM.js.map +1 -0
- package/dist/index.d.ts +2161 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/local-embedding-NZQTILGV.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +334 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -0
- package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
- package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
- package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
- package/dist/plugin-IKQ6IRSJ.js +32 -0
- package/dist/plugin-IKQ6IRSJ.js.map +1 -0
- package/dist/resolve-ASGLBNUC.js +10 -0
- package/dist/resolve-ASGLBNUC.js.map +1 -0
- package/dist/stats-tui-ZY2NQSEA.js +1904 -0
- package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
- package/package.json +96 -0
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +179 -0
- package/src/cli/commands/daemon.ts +100 -0
- package/src/cli/commands/docs.ts +71 -0
- package/src/cli/commands/files.ts +69 -0
- package/src/cli/commands/help.ts +77 -0
- package/src/cli/commands/index.ts +482 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp-export.ts +273 -0
- package/src/cli/commands/mcp.ts +6 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +336 -0
- package/src/cli/commands/search.ts +203 -0
- package/src/cli/commands/stats.ts +68 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +47 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +87 -0
- package/src/cli/factory/config-loader.ts +77 -0
- package/src/cli/factory/index.ts +69 -0
- package/src/cli/factory/plugin-loader.ts +325 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/server-client.ts +178 -0
- package/src/cli/tui/index-tui.tsx +667 -0
- package/src/cli/tui/stats-data.ts +523 -0
- package/src/cli/tui/stats-search.ts +262 -0
- package/src/cli/tui/stats-tui.tsx +1465 -0
- package/src/cli/tui/tree-scanner.ts +650 -0
- package/src/cli/utils.ts +137 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +21 -0
- package/src/db/adapter.ts +112 -0
- package/src/db/metadata.ts +130 -0
- package/src/db/migrations.ts +66 -0
- package/src/db/sqlite-adapter.ts +218 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +81 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +218 -0
- package/src/index.ts +154 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +126 -0
- package/src/lib/math.ts +87 -0
- package/src/lib/provider-key.ts +20 -0
- package/src/lib/prune.ts +71 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +195 -0
- package/src/mcp/workspace-factory.ts +68 -0
- package/src/mcp/workspace-pool.ts +224 -0
- package/src/plugin.ts +381 -0
- package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
- package/src/providers/embeddings/embedding-worker.ts +141 -0
- package/src/providers/embeddings/local-embedding.ts +115 -0
- package/src/providers/embeddings/openai-embedding.ts +167 -0
- package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
- package/src/providers/embeddings/perplexity-embedding.ts +165 -0
- package/src/providers/embeddings/resolve.ts +34 -0
- package/src/providers/pruners/haiku-expander.ts +166 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -0
- package/src/providers/vector/hnsw-index.ts +174 -0
- package/src/providers/vector/hnsw-loader.ts +129 -0
- package/src/search/bm25-boost.ts +69 -0
- package/src/search/context-builder.ts +251 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +61 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +384 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +336 -0
- package/src/services/kv-service.ts +64 -0
- package/src/services/plugin-registry.ts +77 -0
- package/src/services/watch.ts +340 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +493 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3076 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PORT,
|
|
4
|
+
isServerRunning,
|
|
5
|
+
removePid
|
|
6
|
+
} from "./chunk-RDQYDLYZ.js";
|
|
7
|
+
import {
|
|
8
|
+
SUPPORTED_EXTENSIONS,
|
|
9
|
+
VERSION,
|
|
10
|
+
args,
|
|
11
|
+
c,
|
|
12
|
+
contextFromCLI,
|
|
13
|
+
createBrain,
|
|
14
|
+
discoverExternalPlugins,
|
|
15
|
+
findDocsPlugin,
|
|
16
|
+
getConfig,
|
|
17
|
+
getFlag,
|
|
18
|
+
getFlagAll,
|
|
19
|
+
hasFlag,
|
|
20
|
+
isIgnoredDir,
|
|
21
|
+
isIgnoredFile,
|
|
22
|
+
loadConfig,
|
|
23
|
+
printResults,
|
|
24
|
+
registerConfigCollections,
|
|
25
|
+
stripFlags
|
|
26
|
+
} from "./chunk-63GBCDS5.js";
|
|
27
|
+
import "./chunk-M744PCJQ.js";
|
|
28
|
+
import "./chunk-IMJJ2VEM.js";
|
|
29
|
+
import {
|
|
30
|
+
__name,
|
|
31
|
+
__require
|
|
32
|
+
} from "./chunk-WCQVDF3K.js";
|
|
33
|
+
|
|
34
|
+
// src/cli/commands/index.ts
|
|
35
|
+
import * as fs4 from "fs";
|
|
36
|
+
import * as path4 from "path";
|
|
37
|
+
|
|
38
|
+
// src/cli/commands/mcp-export.ts
|
|
39
|
+
import * as fs from "fs";
|
|
40
|
+
import * as path from "path";
|
|
41
|
+
import { fileURLToPath } from "url";
|
|
42
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
var __dirname = path.dirname(__filename);
|
|
44
|
+
var TARGETS = {
|
|
45
|
+
antigravity: {
|
|
46
|
+
configPath: path.join(process.env.HOME ?? "~", ".gemini", "antigravity", "mcp_config.json"),
|
|
47
|
+
label: "Gemini Antigravity"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function buildBrainbankMcpBlock(config) {
|
|
51
|
+
const nodeBin = process.execPath;
|
|
52
|
+
const globalCliJs = path.join(path.dirname(nodeBin), "..", "lib", "node_modules", "brainbank", "dist", "cli.js");
|
|
53
|
+
const localCliJs = path.resolve(__dirname, "..", "..", "dist", "cli.js");
|
|
54
|
+
const resolvedCliJs = fs.existsSync(globalCliJs) ? globalCliJs : localCliJs;
|
|
55
|
+
const env = {};
|
|
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
|
+
if (perplexityKey) env.PERPLEXITY_API_KEY = perplexityKey;
|
|
61
|
+
if (anthropicKey) env.ANTHROPIC_API_KEY = anthropicKey;
|
|
62
|
+
if (openaiKey) env.OPENAI_API_KEY = openaiKey;
|
|
63
|
+
const block = {
|
|
64
|
+
command: nodeBin,
|
|
65
|
+
args: ["--disable-warning=ExperimentalWarning", resolvedCliJs, "mcp"]
|
|
66
|
+
};
|
|
67
|
+
if (Object.keys(env).length > 0) {
|
|
68
|
+
block.env = env;
|
|
69
|
+
}
|
|
70
|
+
return block;
|
|
71
|
+
}
|
|
72
|
+
__name(buildBrainbankMcpBlock, "buildBrainbankMcpBlock");
|
|
73
|
+
function mergeAndWrite(targetPath, block) {
|
|
74
|
+
let existing = { mcpServers: {} };
|
|
75
|
+
const created = !fs.existsSync(targetPath);
|
|
76
|
+
if (!created) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs.readFileSync(targetPath, "utf-8");
|
|
79
|
+
existing = JSON.parse(raw);
|
|
80
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
81
|
+
} catch {
|
|
82
|
+
existing = { mcpServers: {} };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
existing.mcpServers.brainbank = block;
|
|
86
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(targetPath, JSON.stringify(existing, null, 2) + "\n");
|
|
88
|
+
return { created };
|
|
89
|
+
}
|
|
90
|
+
__name(mergeAndWrite, "mergeAndWrite");
|
|
91
|
+
function hasBrainbankMcpEntry(targetPath) {
|
|
92
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(targetPath, "utf-8");
|
|
95
|
+
const config = JSON.parse(raw);
|
|
96
|
+
return !!config.mcpServers?.brainbank;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
__name(hasBrainbankMcpEntry, "hasBrainbankMcpEntry");
|
|
102
|
+
async function autoExportMcp(repoPath) {
|
|
103
|
+
const target = TARGETS.antigravity;
|
|
104
|
+
if (!target) return;
|
|
105
|
+
const antigravityDir = path.dirname(target.configPath);
|
|
106
|
+
if (!fs.existsSync(antigravityDir)) return;
|
|
107
|
+
if (hasBrainbankMcpEntry(target.configPath)) return;
|
|
108
|
+
const config = await getConfig(repoPath);
|
|
109
|
+
const block = buildBrainbankMcpBlock(config);
|
|
110
|
+
mergeAndWrite(target.configPath, block);
|
|
111
|
+
console.log(` ${c.green("\u2713")} Exported MCP config to ${c.dim(path.relative(process.env.HOME ?? "", target.configPath))}`);
|
|
112
|
+
}
|
|
113
|
+
__name(autoExportMcp, "autoExportMcp");
|
|
114
|
+
var BRAINBANK_SECTION_MARKER = "<!-- brainbank:start -->";
|
|
115
|
+
var BRAINBANK_SECTION_END = "<!-- brainbank:end -->";
|
|
116
|
+
var GLOBAL_GEMINI = path.join(process.env.HOME ?? "~", ".gemini", "GEMINI.md");
|
|
117
|
+
function buildGeminiSection() {
|
|
118
|
+
return `${BRAINBANK_SECTION_MARKER}
|
|
119
|
+
|
|
120
|
+
## BrainBank \u2014 Code Intelligence
|
|
121
|
+
|
|
122
|
+
**\`brainbank_context\`** \u2014 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**.
|
|
123
|
+
|
|
124
|
+
**Do NOT use it for:** finding a specific string, variable, function name, or exact match \u2014 use \`grep_search\` for that. BrainBank is for *understanding context*, not for *locating text*.
|
|
125
|
+
|
|
126
|
+
**Params:** \`compact: false\`, \`sources: { code: 25, docs: 0, git: 0 }\`. Only add \`docs\` when the user explicitly asks about documentation.
|
|
127
|
+
|
|
128
|
+
### Output handling \u2014 MANDATORY
|
|
129
|
+
|
|
130
|
+
BrainBank may return results **inline** or save them to an **output file**:
|
|
131
|
+
|
|
132
|
+
- **Inline:** read and proceed normally.
|
|
133
|
+
- **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**:
|
|
134
|
+
|
|
135
|
+
\u{1F4CB} Output: [output.txt](file:///path/to/output.txt) \u2014 _"Could you paste the context output here?"_
|
|
136
|
+
|
|
137
|
+
**NEVER** read the file yourself. **NEVER** continue working. Wait for the user to paste the content. This is not optional.
|
|
138
|
+
|
|
139
|
+
${BRAINBANK_SECTION_END}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
__name(buildGeminiSection, "buildGeminiSection");
|
|
143
|
+
function hasGeminiSection(geminiPath) {
|
|
144
|
+
if (!fs.existsSync(geminiPath)) return false;
|
|
145
|
+
const content = fs.readFileSync(geminiPath, "utf-8");
|
|
146
|
+
return content.includes(BRAINBANK_SECTION_MARKER);
|
|
147
|
+
}
|
|
148
|
+
__name(hasGeminiSection, "hasGeminiSection");
|
|
149
|
+
function appendGeminiSection(geminiPath) {
|
|
150
|
+
const section = buildGeminiSection();
|
|
151
|
+
if (fs.existsSync(geminiPath)) {
|
|
152
|
+
fs.appendFileSync(geminiPath, section);
|
|
153
|
+
} else {
|
|
154
|
+
fs.writeFileSync(geminiPath, `# GEMINI.md
|
|
155
|
+
${section}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
__name(appendGeminiSection, "appendGeminiSection");
|
|
159
|
+
function replaceGeminiSection(geminiPath) {
|
|
160
|
+
const content = fs.readFileSync(geminiPath, "utf-8");
|
|
161
|
+
const startIdx = content.indexOf(BRAINBANK_SECTION_MARKER);
|
|
162
|
+
const endIdx = content.indexOf(BRAINBANK_SECTION_END);
|
|
163
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
164
|
+
const before = content.slice(0, startIdx);
|
|
165
|
+
const after = content.slice(endIdx + BRAINBANK_SECTION_END.length);
|
|
166
|
+
const section = buildGeminiSection();
|
|
167
|
+
fs.writeFileSync(geminiPath, before + section + after);
|
|
168
|
+
}
|
|
169
|
+
__name(replaceGeminiSection, "replaceGeminiSection");
|
|
170
|
+
async function cmdMcpExport() {
|
|
171
|
+
const targetName = args[1] || getFlag("target") || "antigravity";
|
|
172
|
+
const repoPath = getFlag("repo") || ".";
|
|
173
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
174
|
+
const target = TARGETS[targetName];
|
|
175
|
+
if (!target) {
|
|
176
|
+
console.error(c.red(`Unknown export target: ${targetName}`));
|
|
177
|
+
console.error(c.dim(` Available: ${Object.keys(TARGETS).join(", ")}`));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const config = await getConfig(repoPath);
|
|
181
|
+
const block = buildBrainbankMcpBlock(config);
|
|
182
|
+
console.log(c.bold(`
|
|
183
|
+
\u2501\u2501\u2501 MCP Export: ${target.label} \u2501\u2501\u2501
|
|
184
|
+
`));
|
|
185
|
+
const mcpExists = hasBrainbankMcpEntry(target.configPath);
|
|
186
|
+
let writeMcp = true;
|
|
187
|
+
if (mcpExists && !force) {
|
|
188
|
+
console.log(` ${c.yellow("\u25CF")} MCP config already has brainbank entry`);
|
|
189
|
+
const cliPath = block.args.find((a) => !a.startsWith("--")) ?? block.args[0];
|
|
190
|
+
console.log(` ${c.dim(" New:")} ${block.command} ${cliPath}`);
|
|
191
|
+
const envKeys = block.env ? Object.keys(block.env) : [];
|
|
192
|
+
if (envKeys.length > 0) console.log(` ${c.dim(" Keys:")} ${envKeys.join(", ")}`);
|
|
193
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
194
|
+
writeMcp = await confirm({ message: "Override existing brainbank MCP entry?", default: true });
|
|
195
|
+
}
|
|
196
|
+
if (writeMcp) {
|
|
197
|
+
const { created } = mergeAndWrite(target.configPath, block);
|
|
198
|
+
console.log(` ${c.green("\u2713")} ${created ? "Created" : "Updated"} ${c.dim(target.configPath)}`);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` ${c.dim("MCP config \u2014 skipped")}`);
|
|
201
|
+
}
|
|
202
|
+
const geminiHasSection = hasGeminiSection(GLOBAL_GEMINI);
|
|
203
|
+
if (geminiHasSection) {
|
|
204
|
+
if (force) {
|
|
205
|
+
replaceGeminiSection(GLOBAL_GEMINI);
|
|
206
|
+
console.log(` ${c.green("\u2713")} Replaced BrainBank section in ${c.dim("~/.gemini/GEMINI.md")}`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(` ${c.yellow("\u25CF")} ~/.gemini/GEMINI.md already has BrainBank section`);
|
|
209
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
210
|
+
const override = await confirm({ message: "Override existing BrainBank section?", default: false });
|
|
211
|
+
if (override) {
|
|
212
|
+
replaceGeminiSection(GLOBAL_GEMINI);
|
|
213
|
+
console.log(` ${c.green("\u2713")} Replaced BrainBank section in ${c.dim("~/.gemini/GEMINI.md")}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(` ${c.dim("GEMINI.md \u2014 skipped")}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
if (force) {
|
|
220
|
+
appendGeminiSection(GLOBAL_GEMINI);
|
|
221
|
+
console.log(` ${c.green("\u2713")} Added BrainBank section to ${c.dim("~/.gemini/GEMINI.md")}`);
|
|
222
|
+
} else {
|
|
223
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
224
|
+
const addGemini = await confirm({
|
|
225
|
+
message: "Add BrainBank instructions to ~/.gemini/GEMINI.md? (teaches AI tools how to use BrainBank)",
|
|
226
|
+
default: true
|
|
227
|
+
});
|
|
228
|
+
if (addGemini) {
|
|
229
|
+
appendGeminiSection(GLOBAL_GEMINI);
|
|
230
|
+
console.log(` ${c.green("\u2713")} Added BrainBank section to ${c.dim("~/.gemini/GEMINI.md")}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
console.log(`
|
|
235
|
+
${c.dim("Restart your IDE to apply changes.")}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
__name(cmdMcpExport, "cmdMcpExport");
|
|
239
|
+
|
|
240
|
+
// src/cli/commands/scan.ts
|
|
241
|
+
import * as fs2 from "fs";
|
|
242
|
+
import * as path2 from "path";
|
|
243
|
+
import { execSync } from "child_process";
|
|
244
|
+
import picomatch from "picomatch";
|
|
245
|
+
function scanRepo(repoPath) {
|
|
246
|
+
const resolved = path2.resolve(repoPath);
|
|
247
|
+
const config = scanConfig(resolved);
|
|
248
|
+
return {
|
|
249
|
+
repoPath: resolved,
|
|
250
|
+
modules: scanModules(resolved, config),
|
|
251
|
+
config,
|
|
252
|
+
db: scanDb(resolved)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
__name(scanRepo, "scanRepo");
|
|
256
|
+
function scanModules(repoPath, config) {
|
|
257
|
+
return [
|
|
258
|
+
scanCodeModule(repoPath, config.include, config.ignore),
|
|
259
|
+
scanGitModule(repoPath),
|
|
260
|
+
scanDocsModule(repoPath)
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
__name(scanModules, "scanModules");
|
|
264
|
+
function scanCodeModule(repoPath, include, ignore) {
|
|
265
|
+
const byLanguage = /* @__PURE__ */ new Map();
|
|
266
|
+
let total = 0;
|
|
267
|
+
let isIncluded = null;
|
|
268
|
+
let isIgnoredPat = null;
|
|
269
|
+
let includeBases = null;
|
|
270
|
+
if (include?.length) {
|
|
271
|
+
isIncluded = picomatch(include, { dot: true });
|
|
272
|
+
includeBases = include.map((p) => picomatch.scan(p).base).filter((b) => b && b !== ".");
|
|
273
|
+
}
|
|
274
|
+
if (ignore?.length) isIgnoredPat = picomatch(ignore, { dot: true });
|
|
275
|
+
function walk(dir) {
|
|
276
|
+
let entries;
|
|
277
|
+
try {
|
|
278
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
279
|
+
} catch {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
const fullPath = path2.join(dir, entry.name);
|
|
284
|
+
const isDir = entry.isDirectory() || entry.isSymbolicLink() && (() => {
|
|
285
|
+
try {
|
|
286
|
+
return fs2.statSync(fullPath).isDirectory();
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
if (isDir) {
|
|
292
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
293
|
+
if (includeBases && includeBases.length > 0) {
|
|
294
|
+
const relDir = path2.relative(repoPath, fullPath);
|
|
295
|
+
const canMatch = includeBases.some(
|
|
296
|
+
(base) => relDir.startsWith(base) || base.startsWith(relDir)
|
|
297
|
+
);
|
|
298
|
+
if (!canMatch) continue;
|
|
299
|
+
}
|
|
300
|
+
walk(fullPath);
|
|
301
|
+
} else if (entry.isFile()) {
|
|
302
|
+
if (isIgnoredFile(entry.name)) continue;
|
|
303
|
+
const ext = path2.extname(entry.name).toLowerCase();
|
|
304
|
+
const lang = SUPPORTED_EXTENSIONS[ext];
|
|
305
|
+
if (!lang) continue;
|
|
306
|
+
const rel = path2.relative(repoPath, fullPath);
|
|
307
|
+
if (isIncluded && !isIncluded(rel)) continue;
|
|
308
|
+
if (isIgnoredPat && isIgnoredPat(rel)) continue;
|
|
309
|
+
byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
|
|
310
|
+
total++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
__name(walk, "walk");
|
|
315
|
+
walk(repoPath);
|
|
316
|
+
if (total === 0) {
|
|
317
|
+
return { name: "code", available: false, summary: "no supported source files found", icon: "\u{1F4C1}", checked: false, disabled: "nothing to index" };
|
|
318
|
+
}
|
|
319
|
+
const langCount = byLanguage.size;
|
|
320
|
+
const sorted = [...byLanguage.entries()].sort((a, b) => b[1] - a[1]);
|
|
321
|
+
const maxShow = 7;
|
|
322
|
+
const shown = sorted.slice(0, maxShow);
|
|
323
|
+
const remaining = sorted.length - maxShow;
|
|
324
|
+
const details = [];
|
|
325
|
+
for (let i = 0; i < shown.length; i++) {
|
|
326
|
+
const [lang, count] = shown[i];
|
|
327
|
+
const isLast = i === shown.length - 1 && remaining <= 0;
|
|
328
|
+
const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
329
|
+
details.push(`${prefix} ${lang.padEnd(14)} ${count} files`);
|
|
330
|
+
}
|
|
331
|
+
if (remaining > 0) {
|
|
332
|
+
details.push(`\u2514\u2500\u2500 ...and ${remaining} more`);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
name: "code",
|
|
336
|
+
available: true,
|
|
337
|
+
summary: `${total} files (${langCount} language${langCount > 1 ? "s" : ""})`,
|
|
338
|
+
icon: "\u{1F4C1}",
|
|
339
|
+
checked: true,
|
|
340
|
+
details
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
__name(scanCodeModule, "scanCodeModule");
|
|
344
|
+
function scanGitModule(repoPath) {
|
|
345
|
+
const stats = scanGitStats(repoPath);
|
|
346
|
+
if (!stats) {
|
|
347
|
+
return { name: "git", available: false, summary: "no .git directory found", icon: "\u{1F4DC}", checked: false, disabled: "not a git repo" };
|
|
348
|
+
}
|
|
349
|
+
const details = [];
|
|
350
|
+
if (stats.lastMessage) {
|
|
351
|
+
details.push(`Last: ${stats.lastMessage} (${stats.lastDate})`);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
name: "git",
|
|
355
|
+
available: true,
|
|
356
|
+
summary: `${stats.commitCount.toLocaleString()} commits`,
|
|
357
|
+
icon: "\u{1F4DC}",
|
|
358
|
+
checked: true,
|
|
359
|
+
details
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
__name(scanGitModule, "scanGitModule");
|
|
363
|
+
function scanDocsModule(repoPath) {
|
|
364
|
+
const collections = scanDocsCollections(repoPath);
|
|
365
|
+
if (collections.length === 0) {
|
|
366
|
+
return { name: "docs", available: false, summary: "no documents found", icon: "\u{1F4C4}", checked: false, disabled: "no .md/.mdx files" };
|
|
367
|
+
}
|
|
368
|
+
const totalFiles = collections.reduce((s, d) => s + d.fileCount, 0);
|
|
369
|
+
const details = collections.map((d, i) => {
|
|
370
|
+
const isLast = i === collections.length - 1;
|
|
371
|
+
const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
372
|
+
return `${prefix} ${d.name.padEnd(10)} \u2192 ${d.path} (${d.fileCount} files)`;
|
|
373
|
+
});
|
|
374
|
+
return {
|
|
375
|
+
name: "docs",
|
|
376
|
+
available: true,
|
|
377
|
+
summary: `${collections.length} collection${collections.length > 1 ? "s" : ""} (${totalFiles} files)`,
|
|
378
|
+
icon: "\u{1F4C4}",
|
|
379
|
+
checked: true,
|
|
380
|
+
details
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
__name(scanDocsModule, "scanDocsModule");
|
|
384
|
+
function scanGitStats(repoPath) {
|
|
385
|
+
if (!fs2.existsSync(path2.join(repoPath, ".git"))) return null;
|
|
386
|
+
return gitStats(repoPath);
|
|
387
|
+
}
|
|
388
|
+
__name(scanGitStats, "scanGitStats");
|
|
389
|
+
function gitStats(dir) {
|
|
390
|
+
try {
|
|
391
|
+
const count = parseInt(execSync("git rev-list --count HEAD", { cwd: dir, encoding: "utf-8" }).trim(), 10);
|
|
392
|
+
const log = execSync('git log -1 --format="%s|%ar"', { cwd: dir, encoding: "utf-8" }).trim();
|
|
393
|
+
const [lastMessage, lastDate] = log.split("|");
|
|
394
|
+
return { commitCount: count, lastMessage: lastMessage ?? "", lastDate: lastDate ?? "" };
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
__name(gitStats, "gitStats");
|
|
400
|
+
function scanDocsCollections(repoPath) {
|
|
401
|
+
const results = [];
|
|
402
|
+
const seen = /* @__PURE__ */ new Set();
|
|
403
|
+
const configPath = path2.join(repoPath, ".brainbank", "config.json");
|
|
404
|
+
try {
|
|
405
|
+
if (fs2.existsSync(configPath)) {
|
|
406
|
+
const config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
407
|
+
const docsCfg = config?.docs;
|
|
408
|
+
const collections = docsCfg?.collections;
|
|
409
|
+
if (collections) {
|
|
410
|
+
for (const coll of collections) {
|
|
411
|
+
const absPath = path2.resolve(repoPath, coll.path);
|
|
412
|
+
results.push({ name: coll.name, path: coll.path, fileCount: countDocs(absPath) });
|
|
413
|
+
seen.add(absPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
const rootDocs = countDocsShallow(repoPath);
|
|
420
|
+
if (rootDocs > 0) {
|
|
421
|
+
results.push({ name: "(root)", path: ".", fileCount: rootDocs });
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
for (const entry of fs2.readdirSync(repoPath, { withFileTypes: true })) {
|
|
425
|
+
if (!entry.isDirectory()) continue;
|
|
426
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
427
|
+
if (entry.name.startsWith(".")) continue;
|
|
428
|
+
const dirPath = path2.join(repoPath, entry.name);
|
|
429
|
+
if (seen.has(dirPath)) continue;
|
|
430
|
+
const count = countDocs(dirPath);
|
|
431
|
+
if (count > 0) {
|
|
432
|
+
results.push({ name: entry.name, path: `./${entry.name}`, fileCount: count });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
}
|
|
439
|
+
__name(scanDocsCollections, "scanDocsCollections");
|
|
440
|
+
function countDocs(dir) {
|
|
441
|
+
let count = 0;
|
|
442
|
+
try {
|
|
443
|
+
for (const e of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
444
|
+
const ePath = path2.join(dir, e.name);
|
|
445
|
+
const isDir = e.isDirectory() || e.isSymbolicLink() && (() => {
|
|
446
|
+
try {
|
|
447
|
+
return fs2.statSync(ePath).isDirectory();
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
})();
|
|
452
|
+
if (isDir) {
|
|
453
|
+
if (isIgnoredDir(e.name)) continue;
|
|
454
|
+
count += countDocs(ePath);
|
|
455
|
+
} else if ((e.isFile() || e.isSymbolicLink()) && /\.mdx?$/i.test(e.name)) {
|
|
456
|
+
count++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
return count;
|
|
462
|
+
}
|
|
463
|
+
__name(countDocs, "countDocs");
|
|
464
|
+
function countDocsShallow(dir) {
|
|
465
|
+
let count = 0;
|
|
466
|
+
try {
|
|
467
|
+
for (const e of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
468
|
+
if (e.isFile() && /\.mdx?$/i.test(e.name)) count++;
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
return count;
|
|
473
|
+
}
|
|
474
|
+
__name(countDocsShallow, "countDocsShallow");
|
|
475
|
+
function scanConfig(repoPath) {
|
|
476
|
+
const configPath = path2.join(repoPath, ".brainbank", "config.json");
|
|
477
|
+
if (!fs2.existsSync(configPath)) return { exists: false };
|
|
478
|
+
try {
|
|
479
|
+
const config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
480
|
+
const codeCfg = config?.code;
|
|
481
|
+
const rootInclude = config?.include;
|
|
482
|
+
const rootIgnore = config?.ignore;
|
|
483
|
+
const pluginInclude = codeCfg?.include;
|
|
484
|
+
const pluginIgnore = codeCfg?.ignore;
|
|
485
|
+
const include = [...rootInclude ?? [], ...pluginInclude ?? []];
|
|
486
|
+
const ignore = [...rootIgnore ?? [], ...pluginIgnore ?? []];
|
|
487
|
+
return {
|
|
488
|
+
exists: true,
|
|
489
|
+
ignore: ignore.length > 0 ? ignore : void 0,
|
|
490
|
+
include: include.length > 0 ? include : void 0,
|
|
491
|
+
plugins: config?.plugins
|
|
492
|
+
};
|
|
493
|
+
} catch {
|
|
494
|
+
return { exists: false };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
__name(scanConfig, "scanConfig");
|
|
498
|
+
function scanDb(repoPath) {
|
|
499
|
+
const dbPath = path2.join(repoPath, ".brainbank", "data", "brainbank.db");
|
|
500
|
+
if (!fs2.existsSync(dbPath)) return { exists: false, sizeMB: 0 };
|
|
501
|
+
try {
|
|
502
|
+
const stat = fs2.statSync(dbPath);
|
|
503
|
+
return {
|
|
504
|
+
exists: true,
|
|
505
|
+
sizeMB: Math.round(stat.size / 1024 / 1024 * 10) / 10,
|
|
506
|
+
lastModified: stat.mtime
|
|
507
|
+
};
|
|
508
|
+
} catch {
|
|
509
|
+
return { exists: false, sizeMB: 0 };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
__name(scanDb, "scanDb");
|
|
513
|
+
|
|
514
|
+
// src/cli/tui/index-tui.tsx
|
|
515
|
+
import { useState, useMemo, useCallback, useEffect, useRef } from "react";
|
|
516
|
+
import { render, Box, Text, useApp, useInput, useStdout } from "ink";
|
|
517
|
+
|
|
518
|
+
// src/cli/tui/tree-scanner.ts
|
|
519
|
+
import * as fs3 from "fs";
|
|
520
|
+
import * as path3 from "path";
|
|
521
|
+
import { execSync as execSync2 } from "child_process";
|
|
522
|
+
import picomatch2 from "picomatch";
|
|
523
|
+
var EXT_COLORS = {
|
|
524
|
+
".ts": "#519ABA",
|
|
525
|
+
".tsx": "#519ABA",
|
|
526
|
+
".js": "#CBCB41",
|
|
527
|
+
".jsx": "#61DAFB",
|
|
528
|
+
".mjs": "#CBCB41",
|
|
529
|
+
".py": "#4B8BBE",
|
|
530
|
+
".go": "#7FD5EA",
|
|
531
|
+
".rs": "#DEA584",
|
|
532
|
+
".rb": "#CC3E44",
|
|
533
|
+
".java": "#CC3E44",
|
|
534
|
+
".c": "#599EFF",
|
|
535
|
+
".cpp": "#599EFF",
|
|
536
|
+
".h": "#926BD4",
|
|
537
|
+
".cs": "#68217A",
|
|
538
|
+
".php": "#777BB3",
|
|
539
|
+
".swift": "#F05138",
|
|
540
|
+
".kt": "#7F52FF",
|
|
541
|
+
".css": "#42A5F5",
|
|
542
|
+
".scss": "#F06292",
|
|
543
|
+
".html": "#E44D26",
|
|
544
|
+
".vue": "#8DC149",
|
|
545
|
+
".svelte": "#FF3E00",
|
|
546
|
+
".json": "#CBCB41",
|
|
547
|
+
".yaml": "#F44336",
|
|
548
|
+
".yml": "#F44336",
|
|
549
|
+
".md": "#519ABA",
|
|
550
|
+
".sql": "#E0B040",
|
|
551
|
+
".sh": "#89E051",
|
|
552
|
+
".bash": "#89E051",
|
|
553
|
+
".zsh": "#89E051",
|
|
554
|
+
".lua": "#51A0CF",
|
|
555
|
+
".zig": "#F69A1B"
|
|
556
|
+
};
|
|
557
|
+
function getExtColor(ext) {
|
|
558
|
+
return EXT_COLORS[ext] ?? "#7C8DA6";
|
|
559
|
+
}
|
|
560
|
+
__name(getExtColor, "getExtColor");
|
|
561
|
+
function buildFileTree(repoPath, include) {
|
|
562
|
+
const items = [];
|
|
563
|
+
const entries = readSortedEntries(repoPath);
|
|
564
|
+
const hasInclude = include && include.length > 0;
|
|
565
|
+
const isIncluded = hasInclude ? picomatch2(include, { dot: true }) : null;
|
|
566
|
+
const includeBases = hasInclude ? include.map((p) => picomatch2.scan(p).base).filter((b) => b && b !== ".") : null;
|
|
567
|
+
function shouldCheck(relPath, isDir) {
|
|
568
|
+
if (!hasInclude) return true;
|
|
569
|
+
if (!isDir) return isIncluded(relPath);
|
|
570
|
+
if (includeBases) {
|
|
571
|
+
return includeBases.some(
|
|
572
|
+
(base) => relPath.startsWith(base) || base.startsWith(relPath)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
__name(shouldCheck, "shouldCheck");
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (entry.isDir) {
|
|
580
|
+
const dirPath = path3.join(repoPath, entry.name);
|
|
581
|
+
const stats = scanDirStats(dirPath);
|
|
582
|
+
if (stats.total === 0) continue;
|
|
583
|
+
const dirChecked = shouldCheck(entry.name, true);
|
|
584
|
+
items.push({
|
|
585
|
+
path: entry.name,
|
|
586
|
+
name: entry.name,
|
|
587
|
+
depth: 0,
|
|
588
|
+
isDir: true,
|
|
589
|
+
ext: "",
|
|
590
|
+
checked: dirChecked,
|
|
591
|
+
expanded: true,
|
|
592
|
+
hasChildren: true,
|
|
593
|
+
fileCount: stats.total
|
|
594
|
+
});
|
|
595
|
+
const children = readSortedEntries(dirPath);
|
|
596
|
+
for (const child of children) {
|
|
597
|
+
const childRel = `${entry.name}/${child.name}`;
|
|
598
|
+
if (child.isDir) {
|
|
599
|
+
const childAbs = path3.join(dirPath, child.name);
|
|
600
|
+
const cs = scanDirStats(childAbs);
|
|
601
|
+
if (cs.total === 0) continue;
|
|
602
|
+
items.push({
|
|
603
|
+
path: childRel,
|
|
604
|
+
name: child.name,
|
|
605
|
+
depth: 1,
|
|
606
|
+
isDir: true,
|
|
607
|
+
ext: "",
|
|
608
|
+
checked: shouldCheck(childRel, true),
|
|
609
|
+
expanded: false,
|
|
610
|
+
hasChildren: cs.hasSubdirs || cs.total > 0,
|
|
611
|
+
fileCount: cs.total
|
|
612
|
+
});
|
|
613
|
+
} else {
|
|
614
|
+
const ext = path3.extname(child.name).toLowerCase();
|
|
615
|
+
if (!SUPPORTED_EXTENSIONS[ext]) continue;
|
|
616
|
+
items.push({
|
|
617
|
+
path: childRel,
|
|
618
|
+
name: child.name,
|
|
619
|
+
depth: 1,
|
|
620
|
+
isDir: false,
|
|
621
|
+
ext,
|
|
622
|
+
checked: shouldCheck(childRel, false),
|
|
623
|
+
expanded: false,
|
|
624
|
+
hasChildren: false,
|
|
625
|
+
fileCount: 0
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
const ext = path3.extname(entry.name).toLowerCase();
|
|
631
|
+
if (!SUPPORTED_EXTENSIONS[ext]) continue;
|
|
632
|
+
items.push({
|
|
633
|
+
path: entry.name,
|
|
634
|
+
name: entry.name,
|
|
635
|
+
depth: 0,
|
|
636
|
+
isDir: false,
|
|
637
|
+
ext,
|
|
638
|
+
checked: shouldCheck(entry.name, false),
|
|
639
|
+
expanded: false,
|
|
640
|
+
hasChildren: false,
|
|
641
|
+
fileCount: 0
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return items;
|
|
646
|
+
}
|
|
647
|
+
__name(buildFileTree, "buildFileTree");
|
|
648
|
+
function expandDir(items, index, repoPath) {
|
|
649
|
+
const node = items[index];
|
|
650
|
+
if (!node || !node.isDir || node.expanded) return items;
|
|
651
|
+
const absDir = path3.join(repoPath, node.path);
|
|
652
|
+
const entries = readSortedEntries(absDir);
|
|
653
|
+
const children = [];
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
const childRel = `${node.path}/${entry.name}`;
|
|
656
|
+
if (entry.isDir) {
|
|
657
|
+
const childAbs = path3.join(absDir, entry.name);
|
|
658
|
+
const stats = scanDirStats(childAbs);
|
|
659
|
+
if (stats.total === 0) continue;
|
|
660
|
+
children.push({
|
|
661
|
+
path: childRel,
|
|
662
|
+
name: entry.name,
|
|
663
|
+
depth: node.depth + 1,
|
|
664
|
+
isDir: true,
|
|
665
|
+
ext: "",
|
|
666
|
+
checked: node.checked,
|
|
667
|
+
expanded: false,
|
|
668
|
+
hasChildren: stats.hasSubdirs || stats.total > 0,
|
|
669
|
+
fileCount: stats.total
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
const ext = path3.extname(entry.name).toLowerCase();
|
|
673
|
+
if (!SUPPORTED_EXTENSIONS[ext]) continue;
|
|
674
|
+
children.push({
|
|
675
|
+
path: childRel,
|
|
676
|
+
name: entry.name,
|
|
677
|
+
depth: node.depth + 1,
|
|
678
|
+
isDir: false,
|
|
679
|
+
ext,
|
|
680
|
+
checked: node.checked,
|
|
681
|
+
expanded: false,
|
|
682
|
+
hasChildren: false,
|
|
683
|
+
fileCount: 0
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const next = [...items];
|
|
688
|
+
next[index] = { ...node, expanded: true };
|
|
689
|
+
next.splice(index + 1, 0, ...children);
|
|
690
|
+
return next;
|
|
691
|
+
}
|
|
692
|
+
__name(expandDir, "expandDir");
|
|
693
|
+
function collapseDir(items, index) {
|
|
694
|
+
const node = items[index];
|
|
695
|
+
if (!node || !node.isDir || !node.expanded) return items;
|
|
696
|
+
let removeCount = 0;
|
|
697
|
+
for (let i = index + 1; i < items.length; i++) {
|
|
698
|
+
if (items[i].depth <= node.depth) break;
|
|
699
|
+
removeCount++;
|
|
700
|
+
}
|
|
701
|
+
const next = [...items];
|
|
702
|
+
next[index] = { ...node, expanded: false };
|
|
703
|
+
next.splice(index + 1, removeCount);
|
|
704
|
+
return next;
|
|
705
|
+
}
|
|
706
|
+
__name(collapseDir, "collapseDir");
|
|
707
|
+
function toggleDir(items, index) {
|
|
708
|
+
const node = items[index];
|
|
709
|
+
if (!node || !node.isDir) return items;
|
|
710
|
+
const newChecked = !node.checked;
|
|
711
|
+
const next = [...items];
|
|
712
|
+
next[index] = { ...node, checked: newChecked };
|
|
713
|
+
for (let i = index + 1; i < next.length; i++) {
|
|
714
|
+
if (next[i].depth <= node.depth) break;
|
|
715
|
+
next[i] = { ...next[i], checked: newChecked };
|
|
716
|
+
}
|
|
717
|
+
return next;
|
|
718
|
+
}
|
|
719
|
+
__name(toggleDir, "toggleDir");
|
|
720
|
+
function toggleFile(items, index) {
|
|
721
|
+
const node = items[index];
|
|
722
|
+
if (!node || node.isDir) return items;
|
|
723
|
+
const next = [...items];
|
|
724
|
+
next[index] = { ...node, checked: !node.checked };
|
|
725
|
+
return next;
|
|
726
|
+
}
|
|
727
|
+
__name(toggleFile, "toggleFile");
|
|
728
|
+
function setAllDirs(items, checked) {
|
|
729
|
+
return items.map((item) => item.isDir ? { ...item, checked } : { ...item, checked });
|
|
730
|
+
}
|
|
731
|
+
__name(setAllDirs, "setAllDirs");
|
|
732
|
+
function generatePatternsFromTree(items, originalInclude) {
|
|
733
|
+
const include = [];
|
|
734
|
+
const ignore = [];
|
|
735
|
+
const allDirs = items.filter((i) => i.isDir);
|
|
736
|
+
const topDirs = allDirs.filter((i) => i.depth === 0);
|
|
737
|
+
if (topDirs.every((d) => d.checked)) {
|
|
738
|
+
const uncheckedSubs = allDirs.filter((d) => !d.checked && d.depth > 0);
|
|
739
|
+
if (uncheckedSubs.length === 0) return { include: [], ignore: [] };
|
|
740
|
+
for (const item of uncheckedSubs) {
|
|
741
|
+
ignore.push(`${item.path}/**`);
|
|
742
|
+
}
|
|
743
|
+
return { include, ignore };
|
|
744
|
+
}
|
|
745
|
+
if (allDirs.every((d) => !d.checked)) {
|
|
746
|
+
return { include: [], ignore: [] };
|
|
747
|
+
}
|
|
748
|
+
const originalByDir = /* @__PURE__ */ new Map();
|
|
749
|
+
if (originalInclude && originalInclude.length > 0) {
|
|
750
|
+
for (const pattern of originalInclude) {
|
|
751
|
+
const base = pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
752
|
+
const parts = base.split("/");
|
|
753
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
754
|
+
const dirPath = parts.slice(0, i).join("/");
|
|
755
|
+
const existing = originalByDir.get(dirPath) ?? [];
|
|
756
|
+
existing.push(pattern);
|
|
757
|
+
originalByDir.set(dirPath, existing);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function getVisibleChildren(parentIdx) {
|
|
762
|
+
const parent = items[parentIdx];
|
|
763
|
+
const children = [];
|
|
764
|
+
for (let i = parentIdx + 1; i < items.length; i++) {
|
|
765
|
+
if (items[i].depth <= parent.depth) break;
|
|
766
|
+
if (items[i].depth === parent.depth + 1) children.push(items[i]);
|
|
767
|
+
}
|
|
768
|
+
return children;
|
|
769
|
+
}
|
|
770
|
+
__name(getVisibleChildren, "getVisibleChildren");
|
|
771
|
+
function isFullInclusion(idx) {
|
|
772
|
+
const children = getVisibleChildren(idx);
|
|
773
|
+
if (children.length === 0) return true;
|
|
774
|
+
return children.filter((c2) => c2.isDir).every((c2) => c2.checked);
|
|
775
|
+
}
|
|
776
|
+
__name(isFullInclusion, "isFullInclusion");
|
|
777
|
+
for (let i = 0; i < items.length; i++) {
|
|
778
|
+
const item = items[i];
|
|
779
|
+
if (!item.isDir || !item.checked) continue;
|
|
780
|
+
let coveredByParent = false;
|
|
781
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
782
|
+
const ancestor = items[j];
|
|
783
|
+
if (ancestor.isDir && ancestor.depth < item.depth && item.path.startsWith(ancestor.path + "/") && ancestor.checked) {
|
|
784
|
+
if (isFullInclusion(j)) {
|
|
785
|
+
coveredByParent = true;
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (coveredByParent) continue;
|
|
791
|
+
const origPatterns = originalByDir.get(item.path);
|
|
792
|
+
if (origPatterns && origPatterns.length > 0) {
|
|
793
|
+
for (const p of origPatterns) {
|
|
794
|
+
if (!include.includes(p)) {
|
|
795
|
+
include.push(p);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} else {
|
|
799
|
+
include.push(`${item.path}/**`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const includedPrefixes = new Set(include);
|
|
803
|
+
for (const item of items) {
|
|
804
|
+
if (item.isDir || !item.checked) continue;
|
|
805
|
+
const covered = [...includedPrefixes].some((p) => {
|
|
806
|
+
const base = p.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
807
|
+
return item.path.startsWith(base + "/") || item.path === base;
|
|
808
|
+
});
|
|
809
|
+
if (!covered) {
|
|
810
|
+
include.push(item.path);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const item of items) {
|
|
814
|
+
if (item.isDir || item.checked) continue;
|
|
815
|
+
const parentPath = item.path.split("/").slice(0, -1).join("/");
|
|
816
|
+
const parentIncluded = includedPrefixes.has(`${parentPath}/**`);
|
|
817
|
+
if (parentIncluded) {
|
|
818
|
+
ignore.push(item.path);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { include, ignore };
|
|
822
|
+
}
|
|
823
|
+
__name(generatePatternsFromTree, "generatePatternsFromTree");
|
|
824
|
+
function countTotalFiles(items) {
|
|
825
|
+
return items.filter((i) => i.depth === 0 && i.isDir).reduce((s, i) => s + i.fileCount, 0) + items.filter((i) => i.depth === 0 && !i.isDir).length;
|
|
826
|
+
}
|
|
827
|
+
__name(countTotalFiles, "countTotalFiles");
|
|
828
|
+
function scanDirStats(dirPath) {
|
|
829
|
+
const byLang = /* @__PURE__ */ new Map();
|
|
830
|
+
let total = 0;
|
|
831
|
+
let hasSubdirs = false;
|
|
832
|
+
function walk(dir) {
|
|
833
|
+
for (const entry of readDirSafe(dir)) {
|
|
834
|
+
if (isDirEntry(dir, entry)) {
|
|
835
|
+
if (isIgnoredDir(entry.name) || entry.name.startsWith(".")) continue;
|
|
836
|
+
hasSubdirs = true;
|
|
837
|
+
walk(path3.join(dir, entry.name));
|
|
838
|
+
} else if (entry.isFile()) {
|
|
839
|
+
const ext = path3.extname(entry.name).toLowerCase();
|
|
840
|
+
const lang = SUPPORTED_EXTENSIONS[ext];
|
|
841
|
+
if (lang) {
|
|
842
|
+
byLang.set(lang, (byLang.get(lang) ?? 0) + 1);
|
|
843
|
+
total++;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
__name(walk, "walk");
|
|
849
|
+
walk(dirPath);
|
|
850
|
+
return { total, byLang, hasSubdirs };
|
|
851
|
+
}
|
|
852
|
+
__name(scanDirStats, "scanDirStats");
|
|
853
|
+
function readSortedEntries(dir) {
|
|
854
|
+
const raw = readDirSafe(dir);
|
|
855
|
+
const entries = [];
|
|
856
|
+
for (const e of raw) {
|
|
857
|
+
if (e.name.startsWith(".")) continue;
|
|
858
|
+
if (isDirEntry(dir, e)) {
|
|
859
|
+
if (isIgnoredDir(e.name)) continue;
|
|
860
|
+
entries.push({ name: e.name, isDir: true });
|
|
861
|
+
} else if (e.isFile()) {
|
|
862
|
+
entries.push({ name: e.name, isDir: false });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return entries.sort((a, b) => {
|
|
866
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
867
|
+
return a.name.localeCompare(b.name);
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
__name(readSortedEntries, "readSortedEntries");
|
|
871
|
+
function readDirSafe(dir) {
|
|
872
|
+
try {
|
|
873
|
+
return fs3.readdirSync(dir, { withFileTypes: true });
|
|
874
|
+
} catch {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
__name(readDirSafe, "readDirSafe");
|
|
879
|
+
function isDirEntry(parentDir, entry) {
|
|
880
|
+
if (entry.isDirectory()) return true;
|
|
881
|
+
if (entry.isSymbolicLink()) {
|
|
882
|
+
try {
|
|
883
|
+
return fs3.statSync(path3.join(parentDir, entry.name)).isDirectory();
|
|
884
|
+
} catch {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
__name(isDirEntry, "isDirEntry");
|
|
891
|
+
function scanDocsPreview(repoPath) {
|
|
892
|
+
const mdFiles = [];
|
|
893
|
+
function walk(dir, rel) {
|
|
894
|
+
for (const entry of readDirSafe(dir)) {
|
|
895
|
+
if (entry.name.startsWith(".")) continue;
|
|
896
|
+
const fullPath = path3.join(dir, entry.name);
|
|
897
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
898
|
+
if (isDirEntry(dir, entry)) {
|
|
899
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
900
|
+
walk(fullPath, relPath);
|
|
901
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
902
|
+
mdFiles.push(relPath);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
__name(walk, "walk");
|
|
907
|
+
walk(repoPath, "");
|
|
908
|
+
mdFiles.sort();
|
|
909
|
+
if (mdFiles.length === 0) {
|
|
910
|
+
return [{ text: " No markdown files found", dim: true }];
|
|
911
|
+
}
|
|
912
|
+
const lines = [
|
|
913
|
+
{ text: `\u{1F4C4} ${mdFiles.length} markdown files`, bold: true },
|
|
914
|
+
{ text: "" }
|
|
915
|
+
];
|
|
916
|
+
const groups = /* @__PURE__ */ new Map();
|
|
917
|
+
for (const f of mdFiles) {
|
|
918
|
+
const parts = f.split("/");
|
|
919
|
+
const group = parts.length > 1 ? parts[0] : "(root)";
|
|
920
|
+
const list = groups.get(group) || [];
|
|
921
|
+
list.push(f);
|
|
922
|
+
groups.set(group, list);
|
|
923
|
+
}
|
|
924
|
+
for (const [group, files] of groups) {
|
|
925
|
+
if (group !== "(root)") {
|
|
926
|
+
lines.push({ text: ` ${group}/`, bold: true, color: "#E0AF68" });
|
|
927
|
+
}
|
|
928
|
+
for (const f of files) {
|
|
929
|
+
const name = group === "(root)" ? f : f.slice(group.length + 1);
|
|
930
|
+
lines.push({ text: ` MD ${name}`, color: "#519ABA" });
|
|
931
|
+
}
|
|
932
|
+
lines.push({ text: "" });
|
|
933
|
+
}
|
|
934
|
+
return lines;
|
|
935
|
+
}
|
|
936
|
+
__name(scanDocsPreview, "scanDocsPreview");
|
|
937
|
+
function scanGitPreview(repoPath) {
|
|
938
|
+
const gitDir = path3.join(repoPath, ".git");
|
|
939
|
+
if (!fs3.existsSync(gitDir)) {
|
|
940
|
+
return [{ text: " No .git directory found", dim: true }];
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const raw = execSync2(
|
|
944
|
+
'git log --oneline --format="%h %ar %s" -n 20',
|
|
945
|
+
{ cwd: repoPath, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
946
|
+
).trim();
|
|
947
|
+
const commits = raw.split("\n").filter(Boolean);
|
|
948
|
+
if (commits.length === 0) {
|
|
949
|
+
return [{ text: " No commits found", dim: true }];
|
|
950
|
+
}
|
|
951
|
+
let totalStr = "";
|
|
952
|
+
try {
|
|
953
|
+
totalStr = execSync2(
|
|
954
|
+
"git rev-list --count HEAD",
|
|
955
|
+
{ cwd: repoPath, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
956
|
+
).trim();
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
const lines = [
|
|
960
|
+
{ text: `\u{1F4DC} ${totalStr || "?"} commits`, bold: true },
|
|
961
|
+
{ text: "" }
|
|
962
|
+
];
|
|
963
|
+
for (const line of commits) {
|
|
964
|
+
const spaceIdx = line.indexOf(" ");
|
|
965
|
+
const hash = line.slice(0, spaceIdx);
|
|
966
|
+
const rest = line.slice(spaceIdx + 1);
|
|
967
|
+
const timeMatch = rest.match(/^(.+? ago) (.+)$/);
|
|
968
|
+
if (timeMatch) {
|
|
969
|
+
lines.push({
|
|
970
|
+
text: ` ${hash} ${timeMatch[2]}`,
|
|
971
|
+
color: "#C0CAF5"
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
lines.push({ text: ` ${hash} ${rest}`, dim: true });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return lines;
|
|
978
|
+
} catch {
|
|
979
|
+
return [{ text: " Failed to read git log", dim: true }];
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
__name(scanGitPreview, "scanGitPreview");
|
|
983
|
+
|
|
984
|
+
// src/cli/tui/index-tui.tsx
|
|
985
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
986
|
+
var MAX_W = 90;
|
|
987
|
+
var MAX_H = 36;
|
|
988
|
+
var C = {
|
|
989
|
+
aurora: "#7AA2F7",
|
|
990
|
+
success: "#9ECE6A",
|
|
991
|
+
error: "#F7768E",
|
|
992
|
+
warning: "#E0AF68",
|
|
993
|
+
dim: "#565F89",
|
|
994
|
+
text: "#C0CAF5",
|
|
995
|
+
border: "#3B4261",
|
|
996
|
+
cyan: "#7DCFFF",
|
|
997
|
+
dir: "#E0AF68"
|
|
998
|
+
};
|
|
999
|
+
var EMBEDDINGS = [
|
|
1000
|
+
{ value: "perplexity-context", label: "perplexity-context", desc: "best accuracy", badge: "\u2605" },
|
|
1001
|
+
{ value: "perplexity", label: "perplexity", desc: "fast, high quality" },
|
|
1002
|
+
{ value: "openai", label: "openai", desc: "text-embedding-3-small" },
|
|
1003
|
+
{ value: "local", label: "local", desc: "offline, no API key" }
|
|
1004
|
+
];
|
|
1005
|
+
var PRUNERS = [
|
|
1006
|
+
{ value: "haiku", label: "haiku", desc: "AI-powered noise filter", badge: "\u2605" },
|
|
1007
|
+
{ value: "none", label: "none", desc: "no pruning" }
|
|
1008
|
+
];
|
|
1009
|
+
var EXPANDERS = [
|
|
1010
|
+
{ value: "haiku", label: "haiku", desc: "discovers related context", badge: "\u2605" },
|
|
1011
|
+
{ value: "none", label: "none", desc: "no expansion" }
|
|
1012
|
+
];
|
|
1013
|
+
function centerScroll(cursor, total, viewH) {
|
|
1014
|
+
if (total <= viewH) return 0;
|
|
1015
|
+
const half = Math.floor(viewH / 2);
|
|
1016
|
+
const offset = Math.max(0, cursor - half);
|
|
1017
|
+
return Math.min(offset, total - viewH);
|
|
1018
|
+
}
|
|
1019
|
+
__name(centerScroll, "centerScroll");
|
|
1020
|
+
function TreeItemRow({ item, isCursor }) {
|
|
1021
|
+
const indent = " ".repeat(item.depth);
|
|
1022
|
+
const excluded = !item.checked;
|
|
1023
|
+
const ptr = isCursor ? "\u25B8 " : " ";
|
|
1024
|
+
if (item.isDir) {
|
|
1025
|
+
const arrow = item.expanded ? "\u25BE" : "\u25B8";
|
|
1026
|
+
const check2 = item.checked ? "\u2713" : "\u2717";
|
|
1027
|
+
const checkColor2 = item.checked ? C.success : C.error;
|
|
1028
|
+
const nameColor = excluded ? C.dim : isCursor ? C.aurora : C.dir;
|
|
1029
|
+
const count = String(item.fileCount);
|
|
1030
|
+
return /* @__PURE__ */ jsxs(Box, { height: 1, justifyContent: "space-between", children: [
|
|
1031
|
+
/* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
|
|
1032
|
+
/* @__PURE__ */ jsx(Text, { color: isCursor ? C.aurora : C.dim, children: ptr }),
|
|
1033
|
+
/* @__PURE__ */ jsx(Text, { children: indent }),
|
|
1034
|
+
/* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1035
|
+
arrow,
|
|
1036
|
+
" "
|
|
1037
|
+
] }),
|
|
1038
|
+
/* @__PURE__ */ jsxs(Text, { color: checkColor2, bold: true, children: [
|
|
1039
|
+
check2,
|
|
1040
|
+
" "
|
|
1041
|
+
] }),
|
|
1042
|
+
/* @__PURE__ */ jsxs(Text, { color: nameColor, bold: isCursor, children: [
|
|
1043
|
+
item.name,
|
|
1044
|
+
"/"
|
|
1045
|
+
] })
|
|
1046
|
+
] }),
|
|
1047
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: count })
|
|
1048
|
+
] });
|
|
1049
|
+
}
|
|
1050
|
+
const check = item.checked ? "\u2713" : "\u2717";
|
|
1051
|
+
const checkColor = item.checked ? C.success : C.error;
|
|
1052
|
+
return /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
|
|
1053
|
+
/* @__PURE__ */ jsx(Text, { color: isCursor ? C.aurora : C.dim, children: ptr }),
|
|
1054
|
+
/* @__PURE__ */ jsx(Text, { children: indent }),
|
|
1055
|
+
/* @__PURE__ */ jsxs(Text, { color: checkColor, children: [
|
|
1056
|
+
check,
|
|
1057
|
+
" "
|
|
1058
|
+
] }),
|
|
1059
|
+
/* @__PURE__ */ jsx(Text, { color: excluded ? C.dim : getExtColor(item.ext), children: item.name })
|
|
1060
|
+
] }) });
|
|
1061
|
+
}
|
|
1062
|
+
__name(TreeItemRow, "TreeItemRow");
|
|
1063
|
+
function MainScreen({ scan, width, height, onConfirm, externalPreviews }) {
|
|
1064
|
+
const { exit } = useApp();
|
|
1065
|
+
const allMods = scan.modules;
|
|
1066
|
+
const [pane, setPane] = useState("modules");
|
|
1067
|
+
const [checked, setChecked] = useState(() => {
|
|
1068
|
+
const configPlugins = scan.config.plugins;
|
|
1069
|
+
if (configPlugins && configPlugins.length > 0) {
|
|
1070
|
+
return new Set(configPlugins.filter((p) => allMods.some((m) => m.name === p)));
|
|
1071
|
+
}
|
|
1072
|
+
return new Set(allMods.filter((m) => m.available && m.checked).map((m) => m.name));
|
|
1073
|
+
});
|
|
1074
|
+
const firstAvail = allMods.findIndex((m) => m.available);
|
|
1075
|
+
const [modCursor, setModCursor] = useState(Math.max(0, firstAvail));
|
|
1076
|
+
const [treeItems, setTreeItems] = useState(() => buildFileTree(scan.repoPath, scan.config.include));
|
|
1077
|
+
const [treeCursor, setTreeCursor] = useState(0);
|
|
1078
|
+
const [filterText, setFilterText] = useState("");
|
|
1079
|
+
const [isFiltering, setIsFiltering] = useState(false);
|
|
1080
|
+
const isFilteringRef = useRef(false);
|
|
1081
|
+
const startFiltering = useCallback(() => {
|
|
1082
|
+
isFilteringRef.current = true;
|
|
1083
|
+
setIsFiltering(true);
|
|
1084
|
+
setFilterText("");
|
|
1085
|
+
setTreeCursor(0);
|
|
1086
|
+
}, []);
|
|
1087
|
+
const stopFiltering = useCallback(() => {
|
|
1088
|
+
isFilteringRef.current = false;
|
|
1089
|
+
setIsFiltering(false);
|
|
1090
|
+
setFilterText("");
|
|
1091
|
+
setTreeCursor(0);
|
|
1092
|
+
}, []);
|
|
1093
|
+
const docsPreview = useMemo(() => scanDocsPreview(scan.repoPath), [scan.repoPath]);
|
|
1094
|
+
const gitPreview = useMemo(() => scanGitPreview(scan.repoPath), [scan.repoPath]);
|
|
1095
|
+
const allPreviews = useMemo(() => {
|
|
1096
|
+
const map = /* @__PURE__ */ new Map();
|
|
1097
|
+
map.set("docs", docsPreview);
|
|
1098
|
+
map.set("git", gitPreview);
|
|
1099
|
+
if (externalPreviews) {
|
|
1100
|
+
for (const [name, lines] of externalPreviews) {
|
|
1101
|
+
map.set(name, lines);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return map;
|
|
1105
|
+
}, [docsPreview, gitPreview, externalPreviews]);
|
|
1106
|
+
const focusedModName = allMods[modCursor]?.name ?? "code";
|
|
1107
|
+
const panelH = Math.max(6, height - 7);
|
|
1108
|
+
const treeViewH = Math.max(3, panelH - 4);
|
|
1109
|
+
const modUp = /* @__PURE__ */ __name(() => setModCursor((p) => {
|
|
1110
|
+
for (let i = p - 1; i >= 0; i--) if (allMods[i].available) return i;
|
|
1111
|
+
return p;
|
|
1112
|
+
}), "modUp");
|
|
1113
|
+
const modDown = /* @__PURE__ */ __name(() => setModCursor((p) => {
|
|
1114
|
+
for (let i = p + 1; i < allMods.length; i++) if (allMods[i].available) return i;
|
|
1115
|
+
return p;
|
|
1116
|
+
}), "modDown");
|
|
1117
|
+
useInput((input, key) => {
|
|
1118
|
+
const filtering = isFilteringRef.current;
|
|
1119
|
+
if (key.escape) {
|
|
1120
|
+
if (filtering || filterText) {
|
|
1121
|
+
stopFiltering();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
exit();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (input === "q" && !filtering) {
|
|
1128
|
+
exit();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (key.tab && !filtering) {
|
|
1132
|
+
setPane((p) => p === "modules" ? "tree" : "modules");
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (key.return) {
|
|
1136
|
+
if (filtering) {
|
|
1137
|
+
isFilteringRef.current = false;
|
|
1138
|
+
setIsFiltering(false);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const selected = [...checked];
|
|
1142
|
+
if (selected.length === 0) return;
|
|
1143
|
+
const patterns = generatePatternsFromTree(treeItems, scan.config.include);
|
|
1144
|
+
onConfirm(selected, patterns.include, patterns.ignore);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (pane === "modules") {
|
|
1148
|
+
if (key.upArrow || input === "k") {
|
|
1149
|
+
modUp();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (key.downArrow || input === "j") {
|
|
1153
|
+
modDown();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (input === " ") {
|
|
1157
|
+
const mod = allMods[modCursor];
|
|
1158
|
+
if (!mod?.available) return;
|
|
1159
|
+
setChecked((prev) => {
|
|
1160
|
+
const next = new Set(prev);
|
|
1161
|
+
if (next.has(mod.name)) next.delete(mod.name);
|
|
1162
|
+
else next.add(mod.name);
|
|
1163
|
+
return next;
|
|
1164
|
+
});
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (pane === "tree") {
|
|
1169
|
+
if (filtering) {
|
|
1170
|
+
if (key.backspace || key.delete) {
|
|
1171
|
+
setFilterText((prev) => prev.slice(0, -1));
|
|
1172
|
+
setTreeCursor(0);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (input && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
1176
|
+
setFilterText((prev) => prev + input);
|
|
1177
|
+
setTreeCursor(0);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (input === "/" && !filtering) {
|
|
1182
|
+
startFiltering();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (key.upArrow || !filtering && input === "k") {
|
|
1186
|
+
setTreeCursor((p) => Math.max(0, p - 1));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (key.downArrow || !filtering && input === "j") {
|
|
1190
|
+
setTreeCursor((p) => Math.min(filteredItems.length - 1, p + 1));
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (key.rightArrow || !filtering && input === "l") {
|
|
1194
|
+
const item = filteredItems[treeCursor];
|
|
1195
|
+
if (item?.isDir && !item.expanded) {
|
|
1196
|
+
setTreeItems((prev) => expandDir(prev, prev.indexOf(item), scan.repoPath));
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (key.leftArrow || !filtering && input === "h") {
|
|
1201
|
+
const item = filteredItems[treeCursor];
|
|
1202
|
+
if (item?.isDir && item.expanded) {
|
|
1203
|
+
setTreeItems((prev) => collapseDir(prev, prev.indexOf(item)));
|
|
1204
|
+
}
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (input === " ") {
|
|
1208
|
+
const item = filteredItems[treeCursor];
|
|
1209
|
+
if (!item) return;
|
|
1210
|
+
const realIdx = treeItems.indexOf(item);
|
|
1211
|
+
if (realIdx < 0) return;
|
|
1212
|
+
if (item.isDir) setTreeItems((prev) => toggleDir(prev, realIdx));
|
|
1213
|
+
else setTreeItems((prev) => toggleFile(prev, realIdx));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (!filtering && input === "a") {
|
|
1217
|
+
setTreeItems((prev) => setAllDirs(prev, true));
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (!filtering && input === "n") {
|
|
1221
|
+
setTreeItems((prev) => setAllDirs(prev, false));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (!filtering && input === "i") {
|
|
1225
|
+
setTreeItems((prev) => prev.map((it) => ({ ...it, checked: !it.checked })));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
const filteredItems = useMemo(() => {
|
|
1231
|
+
if (!filterText) return treeItems;
|
|
1232
|
+
const lower = filterText.toLowerCase();
|
|
1233
|
+
const matchedPaths = /* @__PURE__ */ new Set();
|
|
1234
|
+
for (const item of treeItems) {
|
|
1235
|
+
if (item.name.toLowerCase().includes(lower)) {
|
|
1236
|
+
matchedPaths.add(item.path);
|
|
1237
|
+
const parts = item.path.split("/");
|
|
1238
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1239
|
+
matchedPaths.add(parts.slice(0, i).join("/"));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return treeItems.filter((item) => matchedPaths.has(item.path));
|
|
1244
|
+
}, [treeItems, filterText]);
|
|
1245
|
+
const totalFiles = countTotalFiles(treeItems);
|
|
1246
|
+
const selectedDirs = treeItems.filter((i) => i.depth === 0 && i.isDir && i.checked).length;
|
|
1247
|
+
const totalDirs = treeItems.filter((i) => i.depth === 0 && i.isDir).length;
|
|
1248
|
+
const visible = filteredItems.slice(
|
|
1249
|
+
centerScroll(treeCursor, filteredItems.length, treeViewH),
|
|
1250
|
+
centerScroll(treeCursor, filteredItems.length, treeViewH) + treeViewH
|
|
1251
|
+
);
|
|
1252
|
+
const scrollOffset = centerScroll(treeCursor, filteredItems.length, treeViewH);
|
|
1253
|
+
const dbInfo = scan.db?.exists ? `${scan.db.sizeMB} MB` : "new";
|
|
1254
|
+
const sidebarW = 30;
|
|
1255
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
1256
|
+
/* @__PURE__ */ jsxs(Box, { paddingX: 2, gap: 2, marginTop: 1, marginBottom: 1, children: [
|
|
1257
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, bold: true, children: "BrainBank" }),
|
|
1258
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: "\xB7" }),
|
|
1259
|
+
/* @__PURE__ */ jsx(Text, { color: C.text, children: scan.repoPath }),
|
|
1260
|
+
/* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1261
|
+
"\xB7 \u{1F4BE} ",
|
|
1262
|
+
dbInfo
|
|
1263
|
+
] })
|
|
1264
|
+
] }),
|
|
1265
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, height: panelH, children: [
|
|
1266
|
+
/* @__PURE__ */ jsxs(
|
|
1267
|
+
Box,
|
|
1268
|
+
{
|
|
1269
|
+
flexDirection: "column",
|
|
1270
|
+
width: sidebarW,
|
|
1271
|
+
borderStyle: "round",
|
|
1272
|
+
borderColor: pane === "modules" ? C.aurora : C.border,
|
|
1273
|
+
children: [
|
|
1274
|
+
/* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: pane === "modules" ? C.aurora : C.dim, bold: true, children: "Modules" }) }),
|
|
1275
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: allMods.map((m, i) => {
|
|
1276
|
+
const avail = m.available;
|
|
1277
|
+
const isCur = i === modCursor;
|
|
1278
|
+
const isChk = checked.has(m.name);
|
|
1279
|
+
const box = !avail ? "\u2500" : isChk ? "\u2713" : " ";
|
|
1280
|
+
const boxCol = !avail ? C.dim : isChk ? C.success : C.dim;
|
|
1281
|
+
const curCol = isCur ? pane === "modules" ? C.aurora : C.dim : "transparent";
|
|
1282
|
+
return /* @__PURE__ */ jsxs(Box, { height: 1, children: [
|
|
1283
|
+
/* @__PURE__ */ jsxs(Text, { color: curCol, children: [
|
|
1284
|
+
isCur ? "\u25B8" : " ",
|
|
1285
|
+
" "
|
|
1286
|
+
] }),
|
|
1287
|
+
/* @__PURE__ */ jsxs(Text, { color: boxCol, children: [
|
|
1288
|
+
"[",
|
|
1289
|
+
box,
|
|
1290
|
+
"] "
|
|
1291
|
+
] }),
|
|
1292
|
+
/* @__PURE__ */ jsx(Text, { color: avail ? C.text : C.dim, children: m.name.charAt(0).toUpperCase() + m.name.slice(1) })
|
|
1293
|
+
] }, m.name);
|
|
1294
|
+
}) }),
|
|
1295
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
1296
|
+
/* @__PURE__ */ jsx(Box, { paddingX: 1, paddingBottom: 1, flexDirection: "column", children: allMods.filter((m) => m.available && checked.has(m.name)).map((m) => /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { color: C.dim, wrap: "truncate", children: m.summary }) }, `s${m.name}`)) })
|
|
1297
|
+
]
|
|
1298
|
+
}
|
|
1299
|
+
),
|
|
1300
|
+
/* @__PURE__ */ jsxs(
|
|
1301
|
+
Box,
|
|
1302
|
+
{
|
|
1303
|
+
flexDirection: "column",
|
|
1304
|
+
flexGrow: 1,
|
|
1305
|
+
borderStyle: "round",
|
|
1306
|
+
borderColor: pane === "tree" ? C.aurora : C.border,
|
|
1307
|
+
marginBottom: 1,
|
|
1308
|
+
children: [
|
|
1309
|
+
/* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", marginBottom: 1, children: [
|
|
1310
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1311
|
+
/* @__PURE__ */ jsx(Text, { color: pane === "tree" ? C.aurora : C.dim, bold: true, children: "Explorer" }),
|
|
1312
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1313
|
+
/* @__PURE__ */ jsx(Text, { color: C.text, children: focusedModName.charAt(0).toUpperCase() + focusedModName.slice(1) })
|
|
1314
|
+
] }),
|
|
1315
|
+
focusedModName === "code" && /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1316
|
+
selectedDirs,
|
|
1317
|
+
"/",
|
|
1318
|
+
totalDirs,
|
|
1319
|
+
" dirs \xB7 ",
|
|
1320
|
+
totalFiles,
|
|
1321
|
+
" files"
|
|
1322
|
+
] })
|
|
1323
|
+
] }),
|
|
1324
|
+
focusedModName === "code" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, height: treeViewH, overflow: "hidden", children: [
|
|
1325
|
+
(isFiltering || filterText) && /* @__PURE__ */ jsxs(Box, { height: 1, marginBottom: 0, children: [
|
|
1326
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, bold: true, children: "/ " }),
|
|
1327
|
+
/* @__PURE__ */ jsx(Text, { color: C.text, children: filterText }),
|
|
1328
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "\u258E" }),
|
|
1329
|
+
filterText && /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1330
|
+
" (",
|
|
1331
|
+
filteredItems.length,
|
|
1332
|
+
" matches)"
|
|
1333
|
+
] })
|
|
1334
|
+
] }),
|
|
1335
|
+
visible.map((item, i) => {
|
|
1336
|
+
const globalIdx = centerScroll(treeCursor, filteredItems.length, treeViewH) + i;
|
|
1337
|
+
return /* @__PURE__ */ jsx(
|
|
1338
|
+
TreeItemRow,
|
|
1339
|
+
{
|
|
1340
|
+
item,
|
|
1341
|
+
isCursor: pane === "tree" && globalIdx === treeCursor
|
|
1342
|
+
},
|
|
1343
|
+
item.path
|
|
1344
|
+
);
|
|
1345
|
+
}),
|
|
1346
|
+
visible.length < treeViewH && Array.from(
|
|
1347
|
+
{ length: treeViewH - visible.length - (isFiltering || filterText ? 1 : 0) },
|
|
1348
|
+
(_, i) => /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { children: " " }) }, `e${i}`)
|
|
1349
|
+
)
|
|
1350
|
+
] }),
|
|
1351
|
+
focusedModName !== "code" && allPreviews.has(focusedModName) && /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 1, height: treeViewH, overflow: "hidden", children: allPreviews.get(focusedModName).slice(0, treeViewH).map((line, i) => /* @__PURE__ */ jsx(
|
|
1352
|
+
Text,
|
|
1353
|
+
{
|
|
1354
|
+
color: line.dim ? C.dim : line.color ?? C.text,
|
|
1355
|
+
bold: line.bold,
|
|
1356
|
+
wrap: "truncate",
|
|
1357
|
+
children: line.text
|
|
1358
|
+
},
|
|
1359
|
+
`p${i}`
|
|
1360
|
+
)) }),
|
|
1361
|
+
focusedModName !== "code" && !allPreviews.has(focusedModName) && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, height: treeViewH, justifyContent: "center", alignItems: "center", children: [
|
|
1362
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: "No preview available" }),
|
|
1363
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: "This plugin will be indexed with default settings" })
|
|
1364
|
+
] }),
|
|
1365
|
+
focusedModName === "code" && filteredItems.length > treeViewH && /* @__PURE__ */ jsx(Box, { paddingX: 2, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1366
|
+
scrollOffset + 1,
|
|
1367
|
+
"\u2013",
|
|
1368
|
+
Math.min(scrollOffset + treeViewH, filteredItems.length),
|
|
1369
|
+
"/",
|
|
1370
|
+
filteredItems.length
|
|
1371
|
+
] }) })
|
|
1372
|
+
]
|
|
1373
|
+
}
|
|
1374
|
+
)
|
|
1375
|
+
] }),
|
|
1376
|
+
/* @__PURE__ */ jsxs(Box, { paddingX: 2, justifyContent: "space-between", marginTop: 1, children: [
|
|
1377
|
+
/* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1378
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "Tab" }),
|
|
1379
|
+
" pane",
|
|
1380
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1381
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "\u2191\u2193" }),
|
|
1382
|
+
" move",
|
|
1383
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1384
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "Space" }),
|
|
1385
|
+
" toggle",
|
|
1386
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1387
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "\u2192\u2190" }),
|
|
1388
|
+
" expand",
|
|
1389
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1390
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "a" }),
|
|
1391
|
+
" all",
|
|
1392
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1393
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "n" }),
|
|
1394
|
+
" none",
|
|
1395
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1396
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "i" }),
|
|
1397
|
+
" invert",
|
|
1398
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: " \xB7 " }),
|
|
1399
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "/" }),
|
|
1400
|
+
" filter"
|
|
1401
|
+
] }),
|
|
1402
|
+
/* @__PURE__ */ jsxs(Text, { color: C.aurora, bold: true, children: [
|
|
1403
|
+
"Enter: ",
|
|
1404
|
+
scan.config.exists ? "Index \u26A1" : "Next \u2192"
|
|
1405
|
+
] })
|
|
1406
|
+
] })
|
|
1407
|
+
] });
|
|
1408
|
+
}
|
|
1409
|
+
__name(MainScreen, "MainScreen");
|
|
1410
|
+
function ConfigPanel({ onDone }) {
|
|
1411
|
+
const { exit } = useApp();
|
|
1412
|
+
const SECTIONS = ["embedding", "pruner", "expander"];
|
|
1413
|
+
const [section, setSection] = useState("embedding");
|
|
1414
|
+
const [embIdx, setEmbIdx] = useState(0);
|
|
1415
|
+
const [prunerIdx, setPrunerIdx] = useState(0);
|
|
1416
|
+
const [expanderIdx, setExpanderIdx] = useState(0);
|
|
1417
|
+
useInput((input, key) => {
|
|
1418
|
+
if (key.escape || input === "q") {
|
|
1419
|
+
exit();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (key.upArrow || input === "k") {
|
|
1423
|
+
if (section === "embedding") setEmbIdx((p) => Math.max(0, p - 1));
|
|
1424
|
+
else if (section === "pruner") setPrunerIdx((p) => Math.max(0, p - 1));
|
|
1425
|
+
else setExpanderIdx((p) => Math.max(0, p - 1));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (key.downArrow || input === "j") {
|
|
1429
|
+
if (section === "embedding") setEmbIdx((p) => Math.min(EMBEDDINGS.length - 1, p + 1));
|
|
1430
|
+
else if (section === "pruner") setPrunerIdx((p) => Math.min(PRUNERS.length - 1, p + 1));
|
|
1431
|
+
else setExpanderIdx((p) => Math.min(EXPANDERS.length - 1, p + 1));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (key.tab) {
|
|
1435
|
+
setSection((p) => {
|
|
1436
|
+
const idx = SECTIONS.indexOf(p);
|
|
1437
|
+
return SECTIONS[(idx + 1) % SECTIONS.length];
|
|
1438
|
+
});
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (key.return) {
|
|
1442
|
+
onDone(EMBEDDINGS[embIdx].value, PRUNERS[prunerIdx].value, EXPANDERS[expanderIdx].value);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
const renderOpt = /* @__PURE__ */ __name((item, i, cur, sel) => /* @__PURE__ */ jsxs(Box, { height: 1, children: [
|
|
1447
|
+
/* @__PURE__ */ jsxs(Text, { color: cur ? C.aurora : C.dim, children: [
|
|
1448
|
+
cur ? "\u25B8" : " ",
|
|
1449
|
+
" "
|
|
1450
|
+
] }),
|
|
1451
|
+
/* @__PURE__ */ jsxs(Text, { color: sel ? C.success : C.dim, children: [
|
|
1452
|
+
sel ? "\u25CF" : "\u25CB",
|
|
1453
|
+
" "
|
|
1454
|
+
] }),
|
|
1455
|
+
/* @__PURE__ */ jsx(Text, { color: cur ? C.text : C.dim, bold: cur, children: item.label.padEnd(22) }),
|
|
1456
|
+
/* @__PURE__ */ jsx(Text, { color: C.dim, children: item.desc }),
|
|
1457
|
+
item.badge ? /* @__PURE__ */ jsxs(Text, { color: C.warning, children: [
|
|
1458
|
+
" ",
|
|
1459
|
+
item.badge
|
|
1460
|
+
] }) : null
|
|
1461
|
+
] }, item.value), "renderOpt");
|
|
1462
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
1463
|
+
/* @__PURE__ */ jsx(Box, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: C.cyan, bold: true, children: "\u2699 First-Time Setup" }) }),
|
|
1464
|
+
/* @__PURE__ */ jsxs(
|
|
1465
|
+
Box,
|
|
1466
|
+
{
|
|
1467
|
+
flexDirection: "column",
|
|
1468
|
+
borderStyle: "round",
|
|
1469
|
+
borderColor: section === "embedding" ? C.aurora : C.border,
|
|
1470
|
+
paddingX: 2,
|
|
1471
|
+
paddingY: 1,
|
|
1472
|
+
children: [
|
|
1473
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: section === "embedding" ? C.aurora : C.dim, bold: true, children: "Embedding Provider" }) }),
|
|
1474
|
+
EMBEDDINGS.map((it, i) => renderOpt(it, i, section === "embedding" && i === embIdx, i === embIdx))
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
),
|
|
1478
|
+
/* @__PURE__ */ jsxs(
|
|
1479
|
+
Box,
|
|
1480
|
+
{
|
|
1481
|
+
flexDirection: "column",
|
|
1482
|
+
borderStyle: "round",
|
|
1483
|
+
borderColor: section === "pruner" ? C.aurora : C.border,
|
|
1484
|
+
paddingX: 2,
|
|
1485
|
+
paddingY: 1,
|
|
1486
|
+
children: [
|
|
1487
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: section === "pruner" ? C.aurora : C.dim, bold: true, children: "Noise Pruner" }) }),
|
|
1488
|
+
PRUNERS.map((it, i) => renderOpt(it, i, section === "pruner" && i === prunerIdx, i === prunerIdx))
|
|
1489
|
+
]
|
|
1490
|
+
}
|
|
1491
|
+
),
|
|
1492
|
+
/* @__PURE__ */ jsxs(
|
|
1493
|
+
Box,
|
|
1494
|
+
{
|
|
1495
|
+
flexDirection: "column",
|
|
1496
|
+
borderStyle: "round",
|
|
1497
|
+
borderColor: section === "expander" ? C.aurora : C.border,
|
|
1498
|
+
paddingX: 2,
|
|
1499
|
+
paddingY: 1,
|
|
1500
|
+
children: [
|
|
1501
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: section === "expander" ? C.aurora : C.dim, bold: true, children: "Context Expander" }) }),
|
|
1502
|
+
EXPANDERS.map((it, i) => renderOpt(it, i, section === "expander" && i === expanderIdx, i === expanderIdx))
|
|
1503
|
+
]
|
|
1504
|
+
}
|
|
1505
|
+
),
|
|
1506
|
+
/* @__PURE__ */ jsx(Box, { paddingX: 1, marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
|
|
1507
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "\u2191\u2193" }),
|
|
1508
|
+
" select \xB7 ",
|
|
1509
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "Tab" }),
|
|
1510
|
+
" section \xB7 ",
|
|
1511
|
+
/* @__PURE__ */ jsx(Text, { color: C.aurora, children: "Enter" }),
|
|
1512
|
+
" start indexing"
|
|
1513
|
+
] }) })
|
|
1514
|
+
] });
|
|
1515
|
+
}
|
|
1516
|
+
__name(ConfigPanel, "ConfigPanel");
|
|
1517
|
+
function IndexApp({ scan, externalPreviews }) {
|
|
1518
|
+
const { exit } = useApp();
|
|
1519
|
+
const { stdout } = useStdout();
|
|
1520
|
+
const [rawW, setRawW] = useState(stdout?.columns || 100);
|
|
1521
|
+
const [rawH, setRawH] = useState(stdout?.rows || 30);
|
|
1522
|
+
const [phase, setPhase] = useState("main");
|
|
1523
|
+
const [selectedModules, setSelectedModules] = useState([]);
|
|
1524
|
+
const [patterns, setPatterns] = useState({ include: [], ignore: [] });
|
|
1525
|
+
const width = Math.min(rawW, MAX_W);
|
|
1526
|
+
const height = Math.min(rawH, MAX_H);
|
|
1527
|
+
useEffect(() => {
|
|
1528
|
+
if (!stdout) return;
|
|
1529
|
+
const onResize = /* @__PURE__ */ __name(() => {
|
|
1530
|
+
setRawW(stdout.columns);
|
|
1531
|
+
setRawH(stdout.rows);
|
|
1532
|
+
}, "onResize");
|
|
1533
|
+
stdout.on("resize", onResize);
|
|
1534
|
+
return () => {
|
|
1535
|
+
stdout.off("resize", onResize);
|
|
1536
|
+
};
|
|
1537
|
+
}, [stdout]);
|
|
1538
|
+
const handleMainConfirm = /* @__PURE__ */ __name((modules, include, ignore) => {
|
|
1539
|
+
setSelectedModules(modules);
|
|
1540
|
+
setPatterns({ include, ignore });
|
|
1541
|
+
if (scan.config.exists) {
|
|
1542
|
+
_lastSelection = { modules, include, ignore };
|
|
1543
|
+
setTimeout(() => exit(), 50);
|
|
1544
|
+
} else {
|
|
1545
|
+
setPhase("config");
|
|
1546
|
+
}
|
|
1547
|
+
}, "handleMainConfirm");
|
|
1548
|
+
const handleConfigDone = /* @__PURE__ */ __name((embedding, pruner, expander) => {
|
|
1549
|
+
_lastSelection = { modules: selectedModules, ...patterns, config: { embedding, pruner, expander } };
|
|
1550
|
+
setTimeout(() => exit(), 50);
|
|
1551
|
+
}, "handleConfigDone");
|
|
1552
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, children: [
|
|
1553
|
+
phase === "main" && /* @__PURE__ */ jsx(
|
|
1554
|
+
MainScreen,
|
|
1555
|
+
{
|
|
1556
|
+
scan,
|
|
1557
|
+
width,
|
|
1558
|
+
height,
|
|
1559
|
+
onConfirm: handleMainConfirm,
|
|
1560
|
+
externalPreviews
|
|
1561
|
+
}
|
|
1562
|
+
),
|
|
1563
|
+
phase === "config" && /* @__PURE__ */ jsx(ConfigPanel, { onDone: handleConfigDone })
|
|
1564
|
+
] });
|
|
1565
|
+
}
|
|
1566
|
+
__name(IndexApp, "IndexApp");
|
|
1567
|
+
var _lastSelection = null;
|
|
1568
|
+
async function runIndexTui(scan, externalPreviews) {
|
|
1569
|
+
_lastSelection = null;
|
|
1570
|
+
const instance = render(/* @__PURE__ */ jsx(IndexApp, { scan, externalPreviews }));
|
|
1571
|
+
await instance.waitUntilExit();
|
|
1572
|
+
return _lastSelection;
|
|
1573
|
+
}
|
|
1574
|
+
__name(runIndexTui, "runIndexTui");
|
|
1575
|
+
|
|
1576
|
+
// src/cli/commands/index.ts
|
|
1577
|
+
async function cmdIndex() {
|
|
1578
|
+
const positional = stripFlags(args);
|
|
1579
|
+
const repoPath = positional[1] || ".";
|
|
1580
|
+
const force = hasFlag("force");
|
|
1581
|
+
const depth = parseInt(getFlag("depth") || "500", 10);
|
|
1582
|
+
const onlyRaw = getFlag("only");
|
|
1583
|
+
const docsPath = getFlag("docs");
|
|
1584
|
+
const skipPrompt = hasFlag("yes") || hasFlag("y");
|
|
1585
|
+
const forceSetup = hasFlag("setup");
|
|
1586
|
+
const scan = scanRepo(repoPath);
|
|
1587
|
+
const configPlugins = scan.config.plugins ?? [];
|
|
1588
|
+
const externalDiscovery = await discoverExternalPlugins(repoPath, configPlugins);
|
|
1589
|
+
let externalPreviews = externalDiscovery.previews;
|
|
1590
|
+
if (externalDiscovery.modules.length > 0) {
|
|
1591
|
+
scan.modules = [...scan.modules, ...externalDiscovery.modules];
|
|
1592
|
+
}
|
|
1593
|
+
let modules;
|
|
1594
|
+
let tuiInclude = [];
|
|
1595
|
+
let tuiIgnore = [];
|
|
1596
|
+
let tuiConfig;
|
|
1597
|
+
if (onlyRaw) {
|
|
1598
|
+
printIndexHeader(scan, depth);
|
|
1599
|
+
modules = onlyRaw.split(",").map((s) => s.trim());
|
|
1600
|
+
} else if (scan.config.plugins && scan.config.plugins.length > 0 && !forceSetup) {
|
|
1601
|
+
printIndexHeader(scan, depth);
|
|
1602
|
+
modules = scan.config.plugins;
|
|
1603
|
+
} else if (skipPrompt) {
|
|
1604
|
+
printIndexHeader(scan, depth);
|
|
1605
|
+
modules = buildDefaultModules(scan);
|
|
1606
|
+
} else {
|
|
1607
|
+
const selection = await runIndexTui(scan, externalPreviews);
|
|
1608
|
+
if (!selection) {
|
|
1609
|
+
console.log(c.dim("\n Cancelled. Exiting.\n"));
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
modules = selection.modules;
|
|
1613
|
+
tuiInclude = selection.include;
|
|
1614
|
+
tuiIgnore = selection.ignore;
|
|
1615
|
+
tuiConfig = selection.config;
|
|
1616
|
+
if (modules.length === 0) {
|
|
1617
|
+
console.log(c.dim("\n Nothing selected. Exiting.\n"));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const oldPlugins = scan.config.plugins ?? [];
|
|
1621
|
+
const removed = oldPlugins.filter((p) => !modules.includes(p));
|
|
1622
|
+
if (removed.length > 0) {
|
|
1623
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Deindexing \u2501\u2501\u2501\n"));
|
|
1624
|
+
for (const mod of removed) {
|
|
1625
|
+
console.log(` ${c.yellow("\u2717")} Removing ${mod} data...`);
|
|
1626
|
+
deindexModule(scan.repoPath, mod);
|
|
1627
|
+
console.log(` ${c.green("\u2713")} ${mod} data cleared`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2501\u2501\u2501\n"));
|
|
1631
|
+
console.log(" Selected modules:");
|
|
1632
|
+
for (const m of modules) {
|
|
1633
|
+
console.log(` ${c.green("\u2713")} ${m}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (tuiInclude.length > 0) {
|
|
1636
|
+
console.log(` Include: ${c.cyan(tuiInclude.join(", "))}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (tuiIgnore.length > 0) {
|
|
1639
|
+
console.log(` Ignore: ${c.yellow(tuiIgnore.join(", "))}`);
|
|
1640
|
+
}
|
|
1641
|
+
console.log("");
|
|
1642
|
+
}
|
|
1643
|
+
if (docsPath && !modules.includes("docs")) {
|
|
1644
|
+
modules.push("docs");
|
|
1645
|
+
}
|
|
1646
|
+
if (tuiConfig) {
|
|
1647
|
+
saveConfigFromTui(scan.repoPath, modules, tuiConfig.embedding, tuiConfig.pruner, tuiConfig.expander, tuiInclude, tuiIgnore);
|
|
1648
|
+
} else if (tuiInclude.length > 0 || tuiIgnore.length > 0) {
|
|
1649
|
+
updateConfigPlugins(scan.repoPath, modules, tuiInclude, tuiIgnore);
|
|
1650
|
+
}
|
|
1651
|
+
console.log(c.bold(`
|
|
1652
|
+
\u2501\u2501\u2501 Indexing: ${modules.join(", ")} \u2501\u2501\u2501`));
|
|
1653
|
+
const ctx = contextFromCLI(repoPath);
|
|
1654
|
+
if (tuiInclude.length > 0 && !ctx.flags?.include) {
|
|
1655
|
+
ctx.flags = { ...ctx.flags, include: tuiInclude.join(",") };
|
|
1656
|
+
}
|
|
1657
|
+
if (tuiIgnore.length > 0 && !ctx.flags?.ignore) {
|
|
1658
|
+
ctx.flags = { ...ctx.flags, ignore: tuiIgnore.join(",") };
|
|
1659
|
+
}
|
|
1660
|
+
const brain = await createBrain(ctx);
|
|
1661
|
+
await brain.initialize();
|
|
1662
|
+
const config = await getConfig(repoPath);
|
|
1663
|
+
await registerConfigCollections(brain, repoPath, config);
|
|
1664
|
+
if (docsPath) {
|
|
1665
|
+
const absDocsPath = path4.resolve(docsPath);
|
|
1666
|
+
const collName = path4.basename(absDocsPath);
|
|
1667
|
+
try {
|
|
1668
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
1669
|
+
await docsPlugin?.addCollection({
|
|
1670
|
+
name: collName,
|
|
1671
|
+
path: absDocsPath,
|
|
1672
|
+
pattern: "**/*.md",
|
|
1673
|
+
ignore: ["deprecated/**", "node_modules/**"]
|
|
1674
|
+
});
|
|
1675
|
+
console.log(c.dim(` Registered docs collection: ${collName}`));
|
|
1676
|
+
} catch {
|
|
1677
|
+
console.log(c.yellow(` Warning: docs module not loaded, skipping --docs`));
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const result = await brain.index({
|
|
1681
|
+
modules,
|
|
1682
|
+
forceReindex: force,
|
|
1683
|
+
pluginOptions: { depth },
|
|
1684
|
+
onProgress: /* @__PURE__ */ __name((stage, msg) => {
|
|
1685
|
+
process.stdout.write(`\r ${c.cyan(stage.toUpperCase())} ${msg} `);
|
|
1686
|
+
}, "onProgress")
|
|
1687
|
+
});
|
|
1688
|
+
console.log("\n");
|
|
1689
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Changes \u2501\u2501\u2501\n"));
|
|
1690
|
+
let hasChanges = false;
|
|
1691
|
+
for (const [name, value] of Object.entries(result)) {
|
|
1692
|
+
if (!value) continue;
|
|
1693
|
+
const v = value;
|
|
1694
|
+
if (typeof v.indexed !== "number") {
|
|
1695
|
+
console.log(` ${c.green("\u2713")} ${name}: done`);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const indexed = v.indexed;
|
|
1699
|
+
const skipped = v.skipped ?? 0;
|
|
1700
|
+
const removed = v.removed ?? 0;
|
|
1701
|
+
const chunks = v.chunks ?? 0;
|
|
1702
|
+
if (indexed > 0 || removed > 0) hasChanges = true;
|
|
1703
|
+
const parts = [];
|
|
1704
|
+
if (indexed > 0) parts.push(c.green(`+${indexed} files (${chunks} chunks)`));
|
|
1705
|
+
if (removed > 0) parts.push(c.red(`\u2212${removed} files`));
|
|
1706
|
+
if (skipped > 0) parts.push(c.dim(`${skipped} unchanged`));
|
|
1707
|
+
console.log(` ${c.bold(name)}: ${parts.join(" ")}`);
|
|
1708
|
+
}
|
|
1709
|
+
if (!hasChanges) {
|
|
1710
|
+
console.log(c.dim(" No changes \u2014 everything up to date"));
|
|
1711
|
+
}
|
|
1712
|
+
const stats = brain.stats();
|
|
1713
|
+
console.log(`
|
|
1714
|
+
${c.bold("Totals")}:`);
|
|
1715
|
+
for (const [name, s] of Object.entries(stats)) {
|
|
1716
|
+
if (!s || typeof s !== "object") continue;
|
|
1717
|
+
const entries = Object.entries(s).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
1718
|
+
console.log(` ${name}: ${entries}`);
|
|
1719
|
+
}
|
|
1720
|
+
brain.close();
|
|
1721
|
+
await autoExportMcp(repoPath);
|
|
1722
|
+
}
|
|
1723
|
+
__name(cmdIndex, "cmdIndex");
|
|
1724
|
+
function printIndexHeader(scan, _depth) {
|
|
1725
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2501\u2501\u2501"));
|
|
1726
|
+
console.log(c.dim(` ${scan.repoPath}
|
|
1727
|
+
`));
|
|
1728
|
+
const plugins = scan.config.plugins ?? [];
|
|
1729
|
+
console.log(` Plugins: ${c.cyan(plugins.join(", "))}`);
|
|
1730
|
+
if (scan.config.include?.length) {
|
|
1731
|
+
console.log("");
|
|
1732
|
+
for (const pattern of scan.config.include) {
|
|
1733
|
+
const exists = validatePattern(scan.repoPath, pattern);
|
|
1734
|
+
const icon = exists ? c.green("\u2713") : c.red("\u2717");
|
|
1735
|
+
const label = exists ? c.dim(pattern) : c.red(pattern);
|
|
1736
|
+
console.log(` ${icon} ${label}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (scan.config.ignore?.length) {
|
|
1740
|
+
console.log("");
|
|
1741
|
+
console.log(c.dim(" Ignore:"));
|
|
1742
|
+
for (const pattern of scan.config.ignore) {
|
|
1743
|
+
console.log(` ${c.yellow("\u2500")} ${c.dim(pattern)}`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
if (scan.db?.exists) {
|
|
1747
|
+
const ago = scan.db.lastModified ? timeSince(scan.db.lastModified) : "";
|
|
1748
|
+
console.log(c.dim(`
|
|
1749
|
+
DB: ${scan.db.sizeMB} MB${ago ? `, last indexed ${ago}` : ""}`));
|
|
1750
|
+
}
|
|
1751
|
+
console.log("");
|
|
1752
|
+
}
|
|
1753
|
+
__name(printIndexHeader, "printIndexHeader");
|
|
1754
|
+
function validatePattern(repoPath, pattern) {
|
|
1755
|
+
const base = pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
1756
|
+
const absPath = path4.join(repoPath, base);
|
|
1757
|
+
try {
|
|
1758
|
+
fs4.statSync(absPath);
|
|
1759
|
+
return true;
|
|
1760
|
+
} catch {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
__name(validatePattern, "validatePattern");
|
|
1765
|
+
function buildDefaultModules(scan) {
|
|
1766
|
+
return scan.modules.filter((m) => m.available && m.checked).map((m) => m.name);
|
|
1767
|
+
}
|
|
1768
|
+
__name(buildDefaultModules, "buildDefaultModules");
|
|
1769
|
+
function timeSince(date) {
|
|
1770
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
|
|
1771
|
+
if (seconds < 60) return "just now";
|
|
1772
|
+
const minutes = Math.floor(seconds / 60);
|
|
1773
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1774
|
+
const hours = Math.floor(minutes / 60);
|
|
1775
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1776
|
+
const days = Math.floor(hours / 24);
|
|
1777
|
+
return `${days}d ago`;
|
|
1778
|
+
}
|
|
1779
|
+
__name(timeSince, "timeSince");
|
|
1780
|
+
var DOC_FOLDERS = ["docs", "doc", "wiki", "documentation", "guides", "notes"];
|
|
1781
|
+
function autoDetectDocCollections(repoPath) {
|
|
1782
|
+
const resolved = path4.resolve(repoPath);
|
|
1783
|
+
const collections = [];
|
|
1784
|
+
for (const folder of DOC_FOLDERS) {
|
|
1785
|
+
const absPath = path4.join(resolved, folder);
|
|
1786
|
+
try {
|
|
1787
|
+
const stat = fs4.statSync(absPath);
|
|
1788
|
+
if (stat.isDirectory()) {
|
|
1789
|
+
const entries = fs4.readdirSync(absPath, { recursive: true });
|
|
1790
|
+
const hasMd = entries.some((e) => typeof e === "string" && /\.md$/i.test(e));
|
|
1791
|
+
if (hasMd) {
|
|
1792
|
+
collections.push({
|
|
1793
|
+
name: folder,
|
|
1794
|
+
path: folder,
|
|
1795
|
+
pattern: "**/*.md",
|
|
1796
|
+
context: `${folder} directory`
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return collections;
|
|
1804
|
+
}
|
|
1805
|
+
__name(autoDetectDocCollections, "autoDetectDocCollections");
|
|
1806
|
+
function saveConfigFromTui(repoPath, modules, embedding, pruner, expander, include, ignore) {
|
|
1807
|
+
const configDir = path4.join(repoPath, ".brainbank");
|
|
1808
|
+
const configPath = path4.join(configDir, "config.json");
|
|
1809
|
+
const config = {
|
|
1810
|
+
plugins: modules,
|
|
1811
|
+
embedding
|
|
1812
|
+
};
|
|
1813
|
+
if (pruner !== "none") {
|
|
1814
|
+
config.pruner = pruner;
|
|
1815
|
+
}
|
|
1816
|
+
if (expander !== "none") {
|
|
1817
|
+
config.expander = expander;
|
|
1818
|
+
}
|
|
1819
|
+
if (include.length > 0) {
|
|
1820
|
+
config.include = include;
|
|
1821
|
+
}
|
|
1822
|
+
if (ignore.length > 0) {
|
|
1823
|
+
config.ignore = ignore;
|
|
1824
|
+
}
|
|
1825
|
+
if (modules.includes("docs")) {
|
|
1826
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
1827
|
+
if (collections.length > 0) {
|
|
1828
|
+
config.docs = { collections };
|
|
1829
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map((dc) => dc.name).join(", ")}`));
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const detectedKeys = {};
|
|
1833
|
+
const needsPerplexity = embedding.startsWith("perplexity");
|
|
1834
|
+
const needsAnthropic = pruner === "haiku" || expander === "haiku";
|
|
1835
|
+
const needsOpenai = embedding === "openai";
|
|
1836
|
+
if (needsPerplexity && process.env.PERPLEXITY_API_KEY) {
|
|
1837
|
+
detectedKeys.perplexity = process.env.PERPLEXITY_API_KEY;
|
|
1838
|
+
}
|
|
1839
|
+
if (needsAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
1840
|
+
detectedKeys.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
1841
|
+
}
|
|
1842
|
+
if (needsOpenai && process.env.OPENAI_API_KEY) {
|
|
1843
|
+
detectedKeys.openai = process.env.OPENAI_API_KEY;
|
|
1844
|
+
}
|
|
1845
|
+
if (Object.keys(detectedKeys).length > 0) {
|
|
1846
|
+
config.keys = detectedKeys;
|
|
1847
|
+
}
|
|
1848
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
1849
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1850
|
+
console.log(c.green(` \u2713 Saved ${path4.relative(process.cwd(), configPath)}`));
|
|
1851
|
+
}
|
|
1852
|
+
__name(saveConfigFromTui, "saveConfigFromTui");
|
|
1853
|
+
function updateConfigPlugins(repoPath, modules, include, ignore) {
|
|
1854
|
+
const configPath = path4.join(repoPath, ".brainbank", "config.json");
|
|
1855
|
+
try {
|
|
1856
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
1857
|
+
const config = JSON.parse(raw);
|
|
1858
|
+
config.plugins = modules;
|
|
1859
|
+
if (include.length > 0) {
|
|
1860
|
+
config.include = include;
|
|
1861
|
+
} else {
|
|
1862
|
+
delete config.include;
|
|
1863
|
+
}
|
|
1864
|
+
if (ignore.length > 0) {
|
|
1865
|
+
config.ignore = ignore;
|
|
1866
|
+
} else {
|
|
1867
|
+
delete config.ignore;
|
|
1868
|
+
}
|
|
1869
|
+
if (modules.includes("docs")) {
|
|
1870
|
+
const existing = config.docs;
|
|
1871
|
+
const hasCollections = existing && Array.isArray(existing.collections) && existing.collections.length > 0;
|
|
1872
|
+
if (!hasCollections) {
|
|
1873
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
1874
|
+
if (collections.length > 0) {
|
|
1875
|
+
config.docs = { ...existing ?? {}, collections };
|
|
1876
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map((dc) => dc.name).join(", ")}`));
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1881
|
+
console.log(c.green(` \u2713 Updated config.json`));
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
__name(updateConfigPlugins, "updateConfigPlugins");
|
|
1886
|
+
function deindexModule(repoPath, moduleName) {
|
|
1887
|
+
const dbPath = path4.join(repoPath, ".brainbank", "data", "brainbank.db");
|
|
1888
|
+
if (!fs4.existsSync(dbPath)) return;
|
|
1889
|
+
let db;
|
|
1890
|
+
try {
|
|
1891
|
+
const sqlite = __require("sqlite");
|
|
1892
|
+
db = new sqlite.DatabaseSync(dbPath);
|
|
1893
|
+
} catch {
|
|
1894
|
+
console.log(c.yellow(` Could not open DB \u2014 skip deindex for ${moduleName}`));
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (!db) return;
|
|
1898
|
+
const tables = {
|
|
1899
|
+
code: [
|
|
1900
|
+
"DELETE FROM code_call_edges",
|
|
1901
|
+
"DELETE FROM code_refs",
|
|
1902
|
+
"DELETE FROM code_symbols",
|
|
1903
|
+
"DELETE FROM code_imports",
|
|
1904
|
+
"DELETE FROM code_vectors",
|
|
1905
|
+
"DELETE FROM code_chunks",
|
|
1906
|
+
"DELETE FROM indexed_files",
|
|
1907
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'code'"
|
|
1908
|
+
],
|
|
1909
|
+
docs: [
|
|
1910
|
+
"DELETE FROM doc_vectors",
|
|
1911
|
+
"DELETE FROM doc_chunks",
|
|
1912
|
+
"DELETE FROM path_contexts",
|
|
1913
|
+
"DELETE FROM collections",
|
|
1914
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'docs'"
|
|
1915
|
+
],
|
|
1916
|
+
git: [
|
|
1917
|
+
"DELETE FROM git_vectors",
|
|
1918
|
+
"DELETE FROM git_commits",
|
|
1919
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'git'"
|
|
1920
|
+
]
|
|
1921
|
+
};
|
|
1922
|
+
const statements = tables[moduleName];
|
|
1923
|
+
if (!statements) {
|
|
1924
|
+
console.log(c.dim(` No known tables for ${moduleName}`));
|
|
1925
|
+
try {
|
|
1926
|
+
db.close();
|
|
1927
|
+
} catch {
|
|
1928
|
+
}
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
for (const sql of statements) {
|
|
1932
|
+
try {
|
|
1933
|
+
db.exec(sql);
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
db.close();
|
|
1939
|
+
} catch {
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
__name(deindexModule, "deindexModule");
|
|
1943
|
+
|
|
1944
|
+
// src/cli/commands/collection.ts
|
|
1945
|
+
async function cmdCollection() {
|
|
1946
|
+
const pos = stripFlags(args);
|
|
1947
|
+
const sub = pos[1];
|
|
1948
|
+
if (sub === "add") {
|
|
1949
|
+
const path6 = pos[2];
|
|
1950
|
+
const name = getFlag("name");
|
|
1951
|
+
const pattern = getFlag("pattern") ?? "**/*.md";
|
|
1952
|
+
const context = getFlag("context");
|
|
1953
|
+
const ignoreRaw = getFlag("ignore");
|
|
1954
|
+
if (!path6 || !name) {
|
|
1955
|
+
console.log(c.red('Usage: brainbank collection add <path> --name <name> [--pattern "**/*.md"] [--ignore "glob"] [--context "description"]'));
|
|
1956
|
+
process.exit(1);
|
|
1957
|
+
}
|
|
1958
|
+
const brain = await createBrain();
|
|
1959
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
1960
|
+
if (!docsPlugin) {
|
|
1961
|
+
console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
await docsPlugin.addCollection({
|
|
1965
|
+
name,
|
|
1966
|
+
path: path6,
|
|
1967
|
+
pattern,
|
|
1968
|
+
ignore: ignoreRaw ? ignoreRaw.split(",") : [],
|
|
1969
|
+
context: context ?? void 0
|
|
1970
|
+
});
|
|
1971
|
+
console.log(c.green(`\u2713 Collection '${name}' added: ${path6} (${pattern})`));
|
|
1972
|
+
if (context) console.log(c.dim(` Context: ${context}`));
|
|
1973
|
+
brain.close();
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (sub === "list") {
|
|
1977
|
+
const brain = await createBrain();
|
|
1978
|
+
await brain.initialize();
|
|
1979
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
1980
|
+
if (!docsPlugin) {
|
|
1981
|
+
console.log(c.yellow(" Docs plugin not loaded."));
|
|
1982
|
+
brain.close();
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const collections = docsPlugin.listCollections();
|
|
1986
|
+
if (collections.length === 0) {
|
|
1987
|
+
console.log(c.yellow(" No collections registered."));
|
|
1988
|
+
} else {
|
|
1989
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Collections \u2501\u2501\u2501\n"));
|
|
1990
|
+
for (const col of collections) {
|
|
1991
|
+
console.log(` ${c.cyan(col.name)} ${c.dim("\u2192")} ${col.path}`);
|
|
1992
|
+
console.log(` Pattern: ${col.pattern ?? "**/*.md"}`);
|
|
1993
|
+
if (col.context) console.log(` Context: ${c.dim(col.context)}`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
brain.close();
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (sub === "remove") {
|
|
2000
|
+
const name = pos[2];
|
|
2001
|
+
if (!name) {
|
|
2002
|
+
console.log(c.red("Usage: brainbank collection remove <name>"));
|
|
2003
|
+
process.exit(1);
|
|
2004
|
+
}
|
|
2005
|
+
const brain = await createBrain();
|
|
2006
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2007
|
+
if (!docsPlugin) {
|
|
2008
|
+
console.log(c.red("Docs plugin not loaded."));
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
await docsPlugin.removeCollection(name);
|
|
2012
|
+
console.log(c.green(`\u2713 Collection '${name}' removed.`));
|
|
2013
|
+
brain.close();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
console.log(c.red("Usage: brainbank collection <add|list|remove>"));
|
|
2017
|
+
process.exit(1);
|
|
2018
|
+
}
|
|
2019
|
+
__name(cmdCollection, "cmdCollection");
|
|
2020
|
+
|
|
2021
|
+
// src/cli/commands/kv.ts
|
|
2022
|
+
async function cmdKv() {
|
|
2023
|
+
const pos = stripFlags(args);
|
|
2024
|
+
const sub = pos[1];
|
|
2025
|
+
if (sub === "add") {
|
|
2026
|
+
const collName = pos[2];
|
|
2027
|
+
const content = pos.slice(3).join(" ");
|
|
2028
|
+
const metaRaw = getFlag("meta");
|
|
2029
|
+
if (!collName || !content) {
|
|
2030
|
+
console.log(c.red(`Usage: brainbank kv add <collection> <content> [--meta '{"key":"val"}']`));
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
}
|
|
2033
|
+
const brain = await createBrain();
|
|
2034
|
+
await brain.initialize();
|
|
2035
|
+
const coll = brain.collection(collName);
|
|
2036
|
+
const meta = metaRaw ? JSON.parse(metaRaw) : {};
|
|
2037
|
+
const id = await coll.add(content, meta);
|
|
2038
|
+
console.log(c.green(`\u2713 Added item #${id} to '${collName}'`));
|
|
2039
|
+
brain.close();
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (sub === "search") {
|
|
2043
|
+
const collName = pos[2];
|
|
2044
|
+
const query = pos.slice(3).join(" ");
|
|
2045
|
+
const k = parseInt(getFlag("k") || "5", 10);
|
|
2046
|
+
const mode = getFlag("mode") || "hybrid";
|
|
2047
|
+
if (!collName || !query) {
|
|
2048
|
+
console.log(c.red("Usage: brainbank kv search <collection> <query> [--k 5] [--mode hybrid|keyword|vector]"));
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
2051
|
+
const brain = await createBrain();
|
|
2052
|
+
await brain.initialize();
|
|
2053
|
+
const coll = brain.collection(collName);
|
|
2054
|
+
const results = await coll.search(query, { k, mode });
|
|
2055
|
+
if (results.length === 0) {
|
|
2056
|
+
console.log(c.yellow(" No results found."));
|
|
2057
|
+
} else {
|
|
2058
|
+
console.log(c.bold(`
|
|
2059
|
+
\u2501\u2501\u2501 ${collName}: "${query}" \u2501\u2501\u2501
|
|
2060
|
+
`));
|
|
2061
|
+
for (const r of results) {
|
|
2062
|
+
const score = Math.round((r.score ?? 0) * 100);
|
|
2063
|
+
console.log(` ${c.cyan(`[${score}%]`)} ${r.content}`);
|
|
2064
|
+
if (Object.keys(r.metadata).length > 0) {
|
|
2065
|
+
console.log(` ${c.dim(JSON.stringify(r.metadata))}`);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
brain.close();
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (sub === "list") {
|
|
2073
|
+
const collName = pos[2];
|
|
2074
|
+
const limit = parseInt(getFlag("limit") || "20", 10);
|
|
2075
|
+
if (!collName) {
|
|
2076
|
+
const brain2 = await createBrain();
|
|
2077
|
+
await brain2.initialize();
|
|
2078
|
+
const names = brain2.listCollectionNames();
|
|
2079
|
+
if (names.length === 0) {
|
|
2080
|
+
console.log(c.yellow(" No KV collections found."));
|
|
2081
|
+
} else {
|
|
2082
|
+
console.log(c.bold("\n\u2501\u2501\u2501 KV Collections \u2501\u2501\u2501\n"));
|
|
2083
|
+
for (const n of names) {
|
|
2084
|
+
const coll2 = brain2.collection(n);
|
|
2085
|
+
console.log(` ${c.cyan(n)} \u2014 ${coll2.count()} items`);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
brain2.close();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const brain = await createBrain();
|
|
2092
|
+
await brain.initialize();
|
|
2093
|
+
const coll = brain.collection(collName);
|
|
2094
|
+
const items = coll.list({ limit });
|
|
2095
|
+
if (items.length === 0) {
|
|
2096
|
+
console.log(c.yellow(` Collection '${collName}' is empty.`));
|
|
2097
|
+
} else {
|
|
2098
|
+
console.log(c.bold(`
|
|
2099
|
+
\u2501\u2501\u2501 ${collName} (${coll.count()} items) \u2501\u2501\u2501
|
|
2100
|
+
`));
|
|
2101
|
+
for (const item of items) {
|
|
2102
|
+
const age = Math.round((Date.now() / 1e3 - item.createdAt) / 60);
|
|
2103
|
+
console.log(` #${item.id} ${c.dim(`(${age}m ago)`)} ${item.content.slice(0, 80)}`);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
brain.close();
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
if (sub === "trim") {
|
|
2110
|
+
const collName = pos[2];
|
|
2111
|
+
const keep = parseInt(getFlag("keep") || "0", 10);
|
|
2112
|
+
if (!collName || keep <= 0) {
|
|
2113
|
+
console.log(c.red("Usage: brainbank kv trim <collection> --keep <n>"));
|
|
2114
|
+
process.exit(1);
|
|
2115
|
+
}
|
|
2116
|
+
const brain = await createBrain();
|
|
2117
|
+
await brain.initialize();
|
|
2118
|
+
const coll = brain.collection(collName);
|
|
2119
|
+
const result = await coll.trim({ keep });
|
|
2120
|
+
console.log(c.green(`\u2713 Trimmed ${result.removed} items from '${collName}' (kept ${keep})`));
|
|
2121
|
+
brain.close();
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
if (sub === "clear") {
|
|
2125
|
+
const collName = pos[2];
|
|
2126
|
+
if (!collName) {
|
|
2127
|
+
console.log(c.red("Usage: brainbank kv clear <collection>"));
|
|
2128
|
+
process.exit(1);
|
|
2129
|
+
}
|
|
2130
|
+
const brain = await createBrain();
|
|
2131
|
+
await brain.initialize();
|
|
2132
|
+
const coll = brain.collection(collName);
|
|
2133
|
+
const before = coll.count();
|
|
2134
|
+
coll.clear();
|
|
2135
|
+
console.log(c.green(`\u2713 Cleared ${before} items from '${collName}'`));
|
|
2136
|
+
brain.close();
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
console.log(c.red("Usage: brainbank kv <add|search|list|trim|clear>"));
|
|
2140
|
+
process.exit(1);
|
|
2141
|
+
}
|
|
2142
|
+
__name(cmdKv, "cmdKv");
|
|
2143
|
+
|
|
2144
|
+
// src/cli/commands/docs.ts
|
|
2145
|
+
async function cmdDocs() {
|
|
2146
|
+
const collection = getFlag("collection");
|
|
2147
|
+
const brain = await createBrain();
|
|
2148
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Docs Index \u2501\u2501\u2501\n"));
|
|
2149
|
+
const opts = {};
|
|
2150
|
+
if (collection) opts.collections = [collection];
|
|
2151
|
+
opts.onProgress = (col, file, cur, total) => {
|
|
2152
|
+
process.stdout.write(`\r ${c.cyan(col)} [${cur}/${total}] ${file} `);
|
|
2153
|
+
};
|
|
2154
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2155
|
+
if (!docsPlugin) {
|
|
2156
|
+
console.log(c.red(" Docs plugin not loaded. Install @brainbank/docs."));
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
const results = await docsPlugin.indexDocs(opts);
|
|
2160
|
+
console.log("\n");
|
|
2161
|
+
for (const [name, stat] of Object.entries(results)) {
|
|
2162
|
+
const removedStr = stat.removed > 0 ? `, ${c.red(String(stat.removed) + " removed")}` : "";
|
|
2163
|
+
console.log(` ${c.green(name)}: ${stat.indexed} indexed, ${stat.skipped} skipped${removedStr}, ${stat.chunks} chunks`);
|
|
2164
|
+
}
|
|
2165
|
+
brain.close();
|
|
2166
|
+
}
|
|
2167
|
+
__name(cmdDocs, "cmdDocs");
|
|
2168
|
+
async function cmdDocSearch() {
|
|
2169
|
+
const query = stripFlags(args).slice(1).join(" ");
|
|
2170
|
+
if (!query) {
|
|
2171
|
+
console.log(c.red("Usage: brainbank dsearch <query>"));
|
|
2172
|
+
process.exit(1);
|
|
2173
|
+
}
|
|
2174
|
+
const brain = await createBrain();
|
|
2175
|
+
const collection = getFlag("collection");
|
|
2176
|
+
const k = parseInt(getFlag("k") || "8", 10);
|
|
2177
|
+
console.log(c.bold(`
|
|
2178
|
+
\u2501\u2501\u2501 BrainBank Doc Search: "${query}" \u2501\u2501\u2501
|
|
2179
|
+
`));
|
|
2180
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2181
|
+
if (!docsPlugin) {
|
|
2182
|
+
console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
|
|
2183
|
+
process.exit(1);
|
|
2184
|
+
}
|
|
2185
|
+
const results = await docsPlugin.search(query, { collection: collection ?? void 0, k });
|
|
2186
|
+
if (results.length === 0) {
|
|
2187
|
+
console.log(c.yellow(" No results found."));
|
|
2188
|
+
brain.close();
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
for (const r of results) {
|
|
2192
|
+
const score = Math.round(r.score * 100);
|
|
2193
|
+
const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
|
|
2194
|
+
console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.type === "document" ? r.metadata.collection ?? "" : ""}]${ctx}`);
|
|
2195
|
+
const preview = r.content.split("\n").slice(0, 4).join("\n");
|
|
2196
|
+
console.log(c.dim(preview));
|
|
2197
|
+
console.log("");
|
|
2198
|
+
}
|
|
2199
|
+
brain.close();
|
|
2200
|
+
}
|
|
2201
|
+
__name(cmdDocSearch, "cmdDocSearch");
|
|
2202
|
+
|
|
2203
|
+
// src/cli/server-client.ts
|
|
2204
|
+
import * as http from "http";
|
|
2205
|
+
async function tryServerContext(options) {
|
|
2206
|
+
const info = isServerRunning();
|
|
2207
|
+
if (!info) return null;
|
|
2208
|
+
try {
|
|
2209
|
+
const body = JSON.stringify({
|
|
2210
|
+
task: options.task,
|
|
2211
|
+
repo: options.repo,
|
|
2212
|
+
sources: options.sources,
|
|
2213
|
+
pathPrefix: options.pathPrefix,
|
|
2214
|
+
affectedFiles: options.affectedFiles
|
|
2215
|
+
});
|
|
2216
|
+
const response = await httpPost(info.port, "/context", body);
|
|
2217
|
+
const data = JSON.parse(response);
|
|
2218
|
+
if (data.error) return null;
|
|
2219
|
+
return data.context ?? null;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
__name(tryServerContext, "tryServerContext");
|
|
2225
|
+
async function tryServerSearch(mode, options) {
|
|
2226
|
+
const info = isServerRunning();
|
|
2227
|
+
if (!info) return null;
|
|
2228
|
+
const endpoint = mode === "search" ? "/search" : mode === "hybrid" ? "/hsearch" : "/ksearch";
|
|
2229
|
+
try {
|
|
2230
|
+
const body = JSON.stringify({
|
|
2231
|
+
query: options.query,
|
|
2232
|
+
repo: options.repo,
|
|
2233
|
+
sources: options.sources,
|
|
2234
|
+
pathPrefix: options.pathPrefix,
|
|
2235
|
+
maxResults: options.maxResults
|
|
2236
|
+
});
|
|
2237
|
+
const response = await httpPost(info.port, endpoint, body);
|
|
2238
|
+
const data = JSON.parse(response);
|
|
2239
|
+
if (data.error) return null;
|
|
2240
|
+
return data.results ?? null;
|
|
2241
|
+
} catch {
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
__name(tryServerSearch, "tryServerSearch");
|
|
2246
|
+
async function serverHealth() {
|
|
2247
|
+
const info = isServerRunning();
|
|
2248
|
+
if (!info) return null;
|
|
2249
|
+
try {
|
|
2250
|
+
const response = await httpGet(info.port, "/health");
|
|
2251
|
+
return JSON.parse(response);
|
|
2252
|
+
} catch {
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
__name(serverHealth, "serverHealth");
|
|
2257
|
+
function httpPost(port, path6, body) {
|
|
2258
|
+
return new Promise((resolve5, reject) => {
|
|
2259
|
+
const req = http.request({
|
|
2260
|
+
hostname: "127.0.0.1",
|
|
2261
|
+
port,
|
|
2262
|
+
path: path6,
|
|
2263
|
+
method: "POST",
|
|
2264
|
+
headers: {
|
|
2265
|
+
"Content-Type": "application/json",
|
|
2266
|
+
"Content-Length": Buffer.byteLength(body)
|
|
2267
|
+
},
|
|
2268
|
+
timeout: 12e4
|
|
2269
|
+
// 2 minutes — context queries can be slow on first load
|
|
2270
|
+
}, (res) => {
|
|
2271
|
+
const chunks = [];
|
|
2272
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
2273
|
+
res.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
|
|
2274
|
+
});
|
|
2275
|
+
req.on("error", reject);
|
|
2276
|
+
req.on("timeout", () => {
|
|
2277
|
+
req.destroy();
|
|
2278
|
+
reject(new Error("Request timed out"));
|
|
2279
|
+
});
|
|
2280
|
+
req.write(body);
|
|
2281
|
+
req.end();
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
__name(httpPost, "httpPost");
|
|
2285
|
+
function httpGet(port, path6) {
|
|
2286
|
+
return new Promise((resolve5, reject) => {
|
|
2287
|
+
const req = http.request({
|
|
2288
|
+
hostname: "127.0.0.1",
|
|
2289
|
+
port,
|
|
2290
|
+
path: path6,
|
|
2291
|
+
method: "GET",
|
|
2292
|
+
timeout: 5e3
|
|
2293
|
+
}, (res) => {
|
|
2294
|
+
const chunks = [];
|
|
2295
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
2296
|
+
res.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
|
|
2297
|
+
});
|
|
2298
|
+
req.on("error", reject);
|
|
2299
|
+
req.on("timeout", () => {
|
|
2300
|
+
req.destroy();
|
|
2301
|
+
reject(new Error("Request timed out"));
|
|
2302
|
+
});
|
|
2303
|
+
req.end();
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
__name(httpGet, "httpGet");
|
|
2307
|
+
|
|
2308
|
+
// src/cli/commands/search.ts
|
|
2309
|
+
function parseSourceFlags() {
|
|
2310
|
+
const NON_SOURCE_FLAGS = /* @__PURE__ */ new Set([
|
|
2311
|
+
"repo",
|
|
2312
|
+
"depth",
|
|
2313
|
+
"collection",
|
|
2314
|
+
"pattern",
|
|
2315
|
+
"context",
|
|
2316
|
+
"name",
|
|
2317
|
+
"keep",
|
|
2318
|
+
"pruner",
|
|
2319
|
+
"only",
|
|
2320
|
+
"docs-path",
|
|
2321
|
+
"mode",
|
|
2322
|
+
"limit",
|
|
2323
|
+
"ignore",
|
|
2324
|
+
"include",
|
|
2325
|
+
"meta",
|
|
2326
|
+
"k",
|
|
2327
|
+
"yes",
|
|
2328
|
+
"y",
|
|
2329
|
+
"force",
|
|
2330
|
+
"verbose",
|
|
2331
|
+
"path"
|
|
2332
|
+
]);
|
|
2333
|
+
const sources = {};
|
|
2334
|
+
const positional = [];
|
|
2335
|
+
for (let i = 0; i < args.length; i++) {
|
|
2336
|
+
if (args[i].startsWith("--")) {
|
|
2337
|
+
const name = args[i].slice(2);
|
|
2338
|
+
if (name === "yes" || name === "force" || name === "verbose") continue;
|
|
2339
|
+
const next = args[i + 1];
|
|
2340
|
+
if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE_FLAGS.has(name)) {
|
|
2341
|
+
sources[name] = parseInt(next, 10);
|
|
2342
|
+
i++;
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
if (NON_SOURCE_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
|
|
2346
|
+
i++;
|
|
2347
|
+
}
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
positional.push(args[i]);
|
|
2351
|
+
}
|
|
2352
|
+
const query = positional.slice(1).join(" ");
|
|
2353
|
+
return { sources, query };
|
|
2354
|
+
}
|
|
2355
|
+
__name(parseSourceFlags, "parseSourceFlags");
|
|
2356
|
+
function parsePaths() {
|
|
2357
|
+
const raw = getFlag("path");
|
|
2358
|
+
if (!raw) return void 0;
|
|
2359
|
+
const paths = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2360
|
+
return paths.length === 1 ? paths[0] : paths;
|
|
2361
|
+
}
|
|
2362
|
+
__name(parsePaths, "parsePaths");
|
|
2363
|
+
function printFilterInfo(sources, pathPrefix) {
|
|
2364
|
+
const parts = [];
|
|
2365
|
+
const entries = Object.entries(sources);
|
|
2366
|
+
if (entries.length > 0) parts.push(...entries.map(([k, v]) => `${k}=${v}`));
|
|
2367
|
+
if (pathPrefix) {
|
|
2368
|
+
const paths = Array.isArray(pathPrefix) ? pathPrefix : [pathPrefix];
|
|
2369
|
+
parts.push(`path=${paths.join(",")}`);
|
|
2370
|
+
}
|
|
2371
|
+
if (parts.length > 0) console.log(c.dim(` Filters: ${parts.join(", ")}`));
|
|
2372
|
+
}
|
|
2373
|
+
__name(printFilterInfo, "printFilterInfo");
|
|
2374
|
+
function buildSearchOptions(sources, pathPrefix) {
|
|
2375
|
+
const opts = {
|
|
2376
|
+
sources: Object.keys(sources).length > 0 ? sources : {},
|
|
2377
|
+
source: "cli"
|
|
2378
|
+
};
|
|
2379
|
+
if (pathPrefix) opts.pathPrefix = pathPrefix;
|
|
2380
|
+
return opts;
|
|
2381
|
+
}
|
|
2382
|
+
__name(buildSearchOptions, "buildSearchOptions");
|
|
2383
|
+
async function cmdSearch() {
|
|
2384
|
+
const { sources, query } = parseSourceFlags();
|
|
2385
|
+
if (!query) {
|
|
2386
|
+
console.log(c.red("Usage: brainbank search <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]"));
|
|
2387
|
+
process.exit(1);
|
|
2388
|
+
}
|
|
2389
|
+
const pathPrefix = parsePaths();
|
|
2390
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2391
|
+
const delegated = await tryServerSearch("search", {
|
|
2392
|
+
query,
|
|
2393
|
+
repo,
|
|
2394
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2395
|
+
pathPrefix
|
|
2396
|
+
});
|
|
2397
|
+
if (delegated) {
|
|
2398
|
+
console.log(c.bold(`
|
|
2399
|
+
\u2501\u2501\u2501 BrainBank Search: "${query}" \u2501\u2501\u2501
|
|
2400
|
+
`));
|
|
2401
|
+
printFilterInfo(sources, pathPrefix);
|
|
2402
|
+
printResults(delegated);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const brain = await createBrain();
|
|
2406
|
+
console.log(c.bold(`
|
|
2407
|
+
\u2501\u2501\u2501 BrainBank Search: "${query}" \u2501\u2501\u2501
|
|
2408
|
+
`));
|
|
2409
|
+
printFilterInfo(sources, pathPrefix);
|
|
2410
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2411
|
+
const results = await brain.search(query, opts);
|
|
2412
|
+
printResults(results);
|
|
2413
|
+
brain.close();
|
|
2414
|
+
}
|
|
2415
|
+
__name(cmdSearch, "cmdSearch");
|
|
2416
|
+
async function cmdHybridSearch() {
|
|
2417
|
+
const { sources, query } = parseSourceFlags();
|
|
2418
|
+
if (!query) {
|
|
2419
|
+
console.log(c.red("Usage: brainbank hsearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>] [--docs <n>]"));
|
|
2420
|
+
process.exit(1);
|
|
2421
|
+
}
|
|
2422
|
+
const pathPrefix = parsePaths();
|
|
2423
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2424
|
+
const delegated = await tryServerSearch("hybrid", {
|
|
2425
|
+
query,
|
|
2426
|
+
repo,
|
|
2427
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2428
|
+
pathPrefix
|
|
2429
|
+
});
|
|
2430
|
+
if (delegated) {
|
|
2431
|
+
console.log(c.bold(`
|
|
2432
|
+
\u2501\u2501\u2501 BrainBank Hybrid Search: "${query}" \u2501\u2501\u2501`));
|
|
2433
|
+
console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion`));
|
|
2434
|
+
printFilterInfo(sources, pathPrefix);
|
|
2435
|
+
console.log("");
|
|
2436
|
+
printResults(delegated);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
const brain = await createBrain();
|
|
2440
|
+
console.log(c.bold(`
|
|
2441
|
+
\u2501\u2501\u2501 BrainBank Hybrid Search: "${query}" \u2501\u2501\u2501`));
|
|
2442
|
+
console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion`));
|
|
2443
|
+
printFilterInfo(sources, pathPrefix);
|
|
2444
|
+
console.log("");
|
|
2445
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2446
|
+
const results = await brain.hybridSearch(query, opts);
|
|
2447
|
+
printResults(results);
|
|
2448
|
+
brain.close();
|
|
2449
|
+
}
|
|
2450
|
+
__name(cmdHybridSearch, "cmdHybridSearch");
|
|
2451
|
+
async function cmdKeywordSearch() {
|
|
2452
|
+
const { sources, query } = parseSourceFlags();
|
|
2453
|
+
if (!query) {
|
|
2454
|
+
console.log(c.red("Usage: brainbank ksearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]"));
|
|
2455
|
+
process.exit(1);
|
|
2456
|
+
}
|
|
2457
|
+
const pathPrefix = parsePaths();
|
|
2458
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2459
|
+
const delegated = await tryServerSearch("keyword", {
|
|
2460
|
+
query,
|
|
2461
|
+
repo,
|
|
2462
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2463
|
+
pathPrefix
|
|
2464
|
+
});
|
|
2465
|
+
if (delegated) {
|
|
2466
|
+
console.log(c.bold(`
|
|
2467
|
+
\u2501\u2501\u2501 BrainBank Keyword Search: "${query}" \u2501\u2501\u2501`));
|
|
2468
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
2469
|
+
printFilterInfo(sources, pathPrefix);
|
|
2470
|
+
console.log("");
|
|
2471
|
+
printResults(delegated, 0.4);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
const brain = await createBrain();
|
|
2475
|
+
await brain.initialize();
|
|
2476
|
+
console.log(c.bold(`
|
|
2477
|
+
\u2501\u2501\u2501 BrainBank Keyword Search: "${query}" \u2501\u2501\u2501`));
|
|
2478
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
2479
|
+
printFilterInfo(sources, pathPrefix);
|
|
2480
|
+
console.log("");
|
|
2481
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2482
|
+
const results = await brain.searchBM25(query, opts);
|
|
2483
|
+
printResults(results, 0.4);
|
|
2484
|
+
brain.close();
|
|
2485
|
+
}
|
|
2486
|
+
__name(cmdKeywordSearch, "cmdKeywordSearch");
|
|
2487
|
+
|
|
2488
|
+
// src/cli/commands/context.ts
|
|
2489
|
+
function parseContextFlags() {
|
|
2490
|
+
const NON_SOURCE = /* @__PURE__ */ new Set([
|
|
2491
|
+
"repo",
|
|
2492
|
+
"depth",
|
|
2493
|
+
"collection",
|
|
2494
|
+
"pattern",
|
|
2495
|
+
"context",
|
|
2496
|
+
"name",
|
|
2497
|
+
"keep",
|
|
2498
|
+
"pruner",
|
|
2499
|
+
"only",
|
|
2500
|
+
"docs-path",
|
|
2501
|
+
"mode",
|
|
2502
|
+
"limit",
|
|
2503
|
+
"ignore",
|
|
2504
|
+
"meta",
|
|
2505
|
+
"k",
|
|
2506
|
+
"yes",
|
|
2507
|
+
"y",
|
|
2508
|
+
"force",
|
|
2509
|
+
"verbose",
|
|
2510
|
+
"path"
|
|
2511
|
+
]);
|
|
2512
|
+
const sources = {};
|
|
2513
|
+
for (let i = 0; i < args.length; i++) {
|
|
2514
|
+
if (!args[i].startsWith("--")) continue;
|
|
2515
|
+
const name = args[i].slice(2);
|
|
2516
|
+
if (name.startsWith("no-")) {
|
|
2517
|
+
sources[name.slice(3)] = 0;
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
const next = args[i + 1];
|
|
2521
|
+
if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE.has(name)) {
|
|
2522
|
+
sources[name] = parseInt(next, 10);
|
|
2523
|
+
i++;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return sources;
|
|
2527
|
+
}
|
|
2528
|
+
__name(parseContextFlags, "parseContextFlags");
|
|
2529
|
+
async function cmdContext() {
|
|
2530
|
+
const pos = stripFlags(args);
|
|
2531
|
+
const sub = pos[1];
|
|
2532
|
+
if (sub === "add") {
|
|
2533
|
+
const collection = pos[2];
|
|
2534
|
+
const path6 = pos[3];
|
|
2535
|
+
const desc = pos.slice(4).join(" ");
|
|
2536
|
+
if (!collection || !path6 || !desc) {
|
|
2537
|
+
console.log(c.red("Usage: brainbank context add <collection> <path> <description>"));
|
|
2538
|
+
process.exit(1);
|
|
2539
|
+
}
|
|
2540
|
+
const brain2 = await createBrain();
|
|
2541
|
+
await brain2.initialize();
|
|
2542
|
+
const docsPlugin = findDocsPlugin(brain2);
|
|
2543
|
+
if (!docsPlugin) {
|
|
2544
|
+
console.log(c.red("Docs plugin not loaded."));
|
|
2545
|
+
process.exit(1);
|
|
2546
|
+
}
|
|
2547
|
+
docsPlugin.addContext(collection, path6, desc);
|
|
2548
|
+
console.log(c.green(`\u2713 Context added: ${collection}:${path6} \u2192 "${desc}"`));
|
|
2549
|
+
brain2.close();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (sub === "list") {
|
|
2553
|
+
const brain2 = await createBrain();
|
|
2554
|
+
await brain2.initialize();
|
|
2555
|
+
const docsPlugin = findDocsPlugin(brain2);
|
|
2556
|
+
if (!docsPlugin) {
|
|
2557
|
+
console.log(c.yellow(" Docs plugin not loaded."));
|
|
2558
|
+
brain2.close();
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
const contexts = docsPlugin.listContexts();
|
|
2562
|
+
if (contexts.length === 0) {
|
|
2563
|
+
console.log(c.yellow(" No contexts configured."));
|
|
2564
|
+
} else {
|
|
2565
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Contexts \u2501\u2501\u2501\n"));
|
|
2566
|
+
for (const ctx of contexts) {
|
|
2567
|
+
console.log(` ${c.cyan(ctx.collection)}:${ctx.path} \u2192 ${c.dim(ctx.context)}`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
brain2.close();
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const task = stripFlags(args).slice(1).join(" ");
|
|
2574
|
+
if (!task) {
|
|
2575
|
+
console.log(c.red("Usage: brainbank context <task description>"));
|
|
2576
|
+
console.log(c.dim(" brainbank context add <collection> <path> <description>"));
|
|
2577
|
+
console.log(c.dim(" brainbank context list"));
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
const sources = parseContextFlags();
|
|
2581
|
+
const rawPath = getFlag("path");
|
|
2582
|
+
const pathPrefix = rawPath ? rawPath.split(",").map((p) => p.trim()).filter(Boolean) : void 0;
|
|
2583
|
+
const normalizedPath = pathPrefix && pathPrefix.length === 1 ? pathPrefix[0] : pathPrefix;
|
|
2584
|
+
const ignorePaths = getFlagAll("ignore");
|
|
2585
|
+
const repo = getFlag("repo");
|
|
2586
|
+
const fields = parseFieldFlags();
|
|
2587
|
+
const serverResult = await tryServerContext({
|
|
2588
|
+
task,
|
|
2589
|
+
repo: repo ?? process.cwd(),
|
|
2590
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2591
|
+
pathPrefix: normalizedPath
|
|
2592
|
+
});
|
|
2593
|
+
if (serverResult !== null) {
|
|
2594
|
+
console.log(serverResult);
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
const brain = await createBrain();
|
|
2598
|
+
const context = await brain.getContext(task, {
|
|
2599
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2600
|
+
pathPrefix: normalizedPath,
|
|
2601
|
+
ignorePaths: ignorePaths.length > 0 ? ignorePaths : void 0,
|
|
2602
|
+
source: "cli",
|
|
2603
|
+
fields: Object.keys(fields).length > 0 ? fields : void 0
|
|
2604
|
+
});
|
|
2605
|
+
console.log(context);
|
|
2606
|
+
brain.close();
|
|
2607
|
+
}
|
|
2608
|
+
__name(cmdContext, "cmdContext");
|
|
2609
|
+
function parseFieldFlags() {
|
|
2610
|
+
const FIELD_BOOLEANS = /* @__PURE__ */ new Set(["lines", "symbols", "compact", "expander"]);
|
|
2611
|
+
const FIELD_NEGATABLE = /* @__PURE__ */ new Set(["callTree", "imports"]);
|
|
2612
|
+
const fields = {};
|
|
2613
|
+
for (let i = 0; i < args.length; i++) {
|
|
2614
|
+
if (!args[i].startsWith("--")) continue;
|
|
2615
|
+
const raw = args[i].slice(2);
|
|
2616
|
+
if (raw.startsWith("no-")) {
|
|
2617
|
+
const name = raw.slice(3);
|
|
2618
|
+
if (FIELD_NEGATABLE.has(name)) {
|
|
2619
|
+
fields[name] = false;
|
|
2620
|
+
}
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const dotIdx = raw.indexOf(".");
|
|
2624
|
+
if (dotIdx > 0) {
|
|
2625
|
+
const fieldName = raw.slice(0, dotIdx);
|
|
2626
|
+
const rest = raw.slice(dotIdx + 1);
|
|
2627
|
+
const eqIdx = rest.indexOf("=");
|
|
2628
|
+
if (eqIdx > 0) {
|
|
2629
|
+
const key = rest.slice(0, eqIdx);
|
|
2630
|
+
const val = parseInt(rest.slice(eqIdx + 1), 10);
|
|
2631
|
+
if (!isNaN(val)) {
|
|
2632
|
+
fields[fieldName] = { [key]: val };
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (FIELD_BOOLEANS.has(raw)) {
|
|
2638
|
+
fields[raw] = true;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
return fields;
|
|
2642
|
+
}
|
|
2643
|
+
__name(parseFieldFlags, "parseFieldFlags");
|
|
2644
|
+
|
|
2645
|
+
// src/cli/commands/files.ts
|
|
2646
|
+
async function cmdFiles() {
|
|
2647
|
+
const patterns = [];
|
|
2648
|
+
const showLines = args.includes("--lines");
|
|
2649
|
+
for (let i = 1; i < args.length; i++) {
|
|
2650
|
+
if (args[i].startsWith("--")) {
|
|
2651
|
+
if (args[i] === "--repo") {
|
|
2652
|
+
i++;
|
|
2653
|
+
}
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
patterns.push(args[i]);
|
|
2657
|
+
}
|
|
2658
|
+
if (patterns.length === 0) {
|
|
2659
|
+
console.log(c.red("Usage: brainbank files <path|glob> [...paths] [--lines]"));
|
|
2660
|
+
console.log(c.dim(" Exact: brainbank files src/auth/login.ts"));
|
|
2661
|
+
console.log(c.dim(" Directory: brainbank files src/graph/"));
|
|
2662
|
+
console.log(c.dim(' Glob: brainbank files "src/**/*.service.ts"'));
|
|
2663
|
+
console.log(c.dim(" Fuzzy: brainbank files plugin.ts"));
|
|
2664
|
+
console.log(c.dim(" Lines: brainbank files src/plugin.ts --lines"));
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
const brain = await createBrain();
|
|
2668
|
+
await brain.initialize();
|
|
2669
|
+
const results = brain.resolveFiles(patterns);
|
|
2670
|
+
if (results.length === 0) {
|
|
2671
|
+
console.log(c.yellow("No matching files found in the index."));
|
|
2672
|
+
console.log(c.dim("Run `brainbank index` first to index your codebase."));
|
|
2673
|
+
brain.close();
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
for (const r of results) {
|
|
2677
|
+
const meta = r.metadata;
|
|
2678
|
+
const startLine = meta.startLine ?? 1;
|
|
2679
|
+
console.log(c.bold(`
|
|
2680
|
+
\u2500\u2500 ${r.filePath} \u2500\u2500
|
|
2681
|
+
`));
|
|
2682
|
+
if (showLines) {
|
|
2683
|
+
const codeLines = r.content.split("\n");
|
|
2684
|
+
const pad = String(startLine + codeLines.length - 1).length;
|
|
2685
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
2686
|
+
const lineNum = c.dim(`${String(startLine + i).padStart(pad)}|`);
|
|
2687
|
+
console.log(`${lineNum} ${codeLines[i]}`);
|
|
2688
|
+
}
|
|
2689
|
+
} else {
|
|
2690
|
+
console.log(r.content);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
console.log(c.dim(`
|
|
2694
|
+
${results.length} file(s) resolved.`));
|
|
2695
|
+
brain.close();
|
|
2696
|
+
}
|
|
2697
|
+
__name(cmdFiles, "cmdFiles");
|
|
2698
|
+
|
|
2699
|
+
// src/cli/commands/stats.ts
|
|
2700
|
+
import * as path5 from "path";
|
|
2701
|
+
function formatStatKey(key) {
|
|
2702
|
+
return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase()).padEnd(16);
|
|
2703
|
+
}
|
|
2704
|
+
__name(formatStatKey, "formatStatKey");
|
|
2705
|
+
async function cmdStats() {
|
|
2706
|
+
const plain = args.includes("--plain");
|
|
2707
|
+
if (!plain) {
|
|
2708
|
+
try {
|
|
2709
|
+
const repoPath = path5.resolve(getFlag("repo") ?? process.cwd());
|
|
2710
|
+
const dbPath = path5.join(repoPath, ".brainbank", "data", "brainbank.db");
|
|
2711
|
+
const configPath = path5.join(repoPath, ".brainbank", "config.json");
|
|
2712
|
+
const { runStatsTui } = await import("./stats-tui-ZY2NQSEA.js");
|
|
2713
|
+
await runStatsTui(dbPath, repoPath, configPath);
|
|
2714
|
+
return;
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
if (err instanceof Error && err.message.includes("ENOENT")) {
|
|
2717
|
+
console.log(c.yellow("No BrainBank database found. Run `brainbank index` first.\n"));
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const brain = await createBrain();
|
|
2723
|
+
await brain.initialize();
|
|
2724
|
+
const s = brain.stats();
|
|
2725
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Stats \u2501\u2501\u2501\n"));
|
|
2726
|
+
console.log(` ${c.cyan("Plugins")}: ${brain.plugins.join(", ")}
|
|
2727
|
+
`);
|
|
2728
|
+
for (const [name, pluginStats] of Object.entries(s)) {
|
|
2729
|
+
if (!pluginStats) continue;
|
|
2730
|
+
console.log(` ${c.cyan(name)}`);
|
|
2731
|
+
for (const [key, value] of Object.entries(pluginStats)) {
|
|
2732
|
+
console.log(` ${formatStatKey(key)}${value}`);
|
|
2733
|
+
}
|
|
2734
|
+
console.log("");
|
|
2735
|
+
}
|
|
2736
|
+
const kvNames = brain.listCollectionNames();
|
|
2737
|
+
if (kvNames.length > 0) {
|
|
2738
|
+
console.log(` ${c.cyan("KV Collections")}`);
|
|
2739
|
+
for (const name of kvNames) {
|
|
2740
|
+
const coll = brain.collection(name);
|
|
2741
|
+
console.log(` ${name}: ${coll.count()} items`);
|
|
2742
|
+
}
|
|
2743
|
+
console.log("");
|
|
2744
|
+
}
|
|
2745
|
+
brain.close();
|
|
2746
|
+
}
|
|
2747
|
+
__name(cmdStats, "cmdStats");
|
|
2748
|
+
|
|
2749
|
+
// src/cli/commands/reembed.ts
|
|
2750
|
+
async function cmdReembed() {
|
|
2751
|
+
const brain = await createBrain();
|
|
2752
|
+
await brain.initialize();
|
|
2753
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Re-embed \u2501\u2501\u2501\n"));
|
|
2754
|
+
console.log(c.dim(" Regenerating vectors with current embedding provider..."));
|
|
2755
|
+
console.log(c.dim(" Text, FTS, and metadata remain unchanged.\n"));
|
|
2756
|
+
const result = await brain.reembed({
|
|
2757
|
+
onProgress: /* @__PURE__ */ __name((table, current, total) => {
|
|
2758
|
+
process.stdout.write(`\r ${c.cyan(table.padEnd(8))} ${current}/${total}`);
|
|
2759
|
+
}, "onProgress")
|
|
2760
|
+
});
|
|
2761
|
+
console.log("\n");
|
|
2762
|
+
for (const [name, count] of Object.entries(result.counts)) {
|
|
2763
|
+
if (count > 0) {
|
|
2764
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2765
|
+
console.log(` ${c.green("\u2713")} ${label.padEnd(8)} ${count} vectors`);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
console.log(`
|
|
2769
|
+
${c.bold("Total")}: ${result.total} vectors regenerated
|
|
2770
|
+
`);
|
|
2771
|
+
brain.close();
|
|
2772
|
+
}
|
|
2773
|
+
__name(cmdReembed, "cmdReembed");
|
|
2774
|
+
|
|
2775
|
+
// src/cli/commands/watch.ts
|
|
2776
|
+
async function cmdWatch() {
|
|
2777
|
+
const brain = await createBrain();
|
|
2778
|
+
await brain.initialize();
|
|
2779
|
+
const config = await loadConfig(brain.config.repoPath);
|
|
2780
|
+
const codeIgnore = config?.code?.ignore ?? [];
|
|
2781
|
+
const codeInclude = config?.code?.include ?? [];
|
|
2782
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Watch \u2501\u2501\u2501\n"));
|
|
2783
|
+
console.log(c.dim(` Watching ${brain.config.repoPath} for changes...`));
|
|
2784
|
+
if (codeInclude.length > 0) {
|
|
2785
|
+
console.log(c.dim(` Include: ${codeInclude.join(", ")}`));
|
|
2786
|
+
}
|
|
2787
|
+
if (codeIgnore.length > 0) {
|
|
2788
|
+
console.log(c.dim(` Ignoring: ${codeIgnore.join(", ")}`));
|
|
2789
|
+
}
|
|
2790
|
+
console.log(c.dim(" Press Ctrl+C to stop.\n"));
|
|
2791
|
+
const watcher = brain.watch({
|
|
2792
|
+
debounceMs: 2e3,
|
|
2793
|
+
ignore: codeIgnore,
|
|
2794
|
+
onIndex: /* @__PURE__ */ __name((sourceId, pluginName) => {
|
|
2795
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
2796
|
+
console.log(` ${c.dim(ts)} ${c.green("\u2713")} ${c.cyan(pluginName)}: ${sourceId}`);
|
|
2797
|
+
}, "onIndex"),
|
|
2798
|
+
onError: /* @__PURE__ */ __name((err) => {
|
|
2799
|
+
console.error(` ${c.red("\u2717")} ${err.message}`);
|
|
2800
|
+
}, "onError")
|
|
2801
|
+
});
|
|
2802
|
+
process.on("SIGINT", () => {
|
|
2803
|
+
console.log(c.dim("\n Stopping watcher..."));
|
|
2804
|
+
watcher.close();
|
|
2805
|
+
brain.close();
|
|
2806
|
+
process.exit(0);
|
|
2807
|
+
});
|
|
2808
|
+
await new Promise(() => {
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
__name(cmdWatch, "cmdWatch");
|
|
2812
|
+
|
|
2813
|
+
// src/cli/commands/mcp.ts
|
|
2814
|
+
async function cmdMcp() {
|
|
2815
|
+
await import("./mcp.js");
|
|
2816
|
+
}
|
|
2817
|
+
__name(cmdMcp, "cmdMcp");
|
|
2818
|
+
|
|
2819
|
+
// src/cli/commands/daemon.ts
|
|
2820
|
+
async function cmdDaemon() {
|
|
2821
|
+
const pos = stripFlags(args);
|
|
2822
|
+
const sub = pos[1];
|
|
2823
|
+
if (sub === "stop") return stopDaemon();
|
|
2824
|
+
if (sub === "restart") {
|
|
2825
|
+
stopDaemon();
|
|
2826
|
+
return forkDaemon();
|
|
2827
|
+
}
|
|
2828
|
+
if (sub === "start") return forkDaemon();
|
|
2829
|
+
return startForeground();
|
|
2830
|
+
}
|
|
2831
|
+
__name(cmdDaemon, "cmdDaemon");
|
|
2832
|
+
async function startForeground() {
|
|
2833
|
+
const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
|
|
2834
|
+
const { HttpServer } = await import("./http-server-QUXHLWUM.js");
|
|
2835
|
+
const server = new HttpServer({
|
|
2836
|
+
port,
|
|
2837
|
+
factory: /* @__PURE__ */ __name(async (repoPath) => {
|
|
2838
|
+
const brain = await createBrain(repoPath);
|
|
2839
|
+
await brain.initialize();
|
|
2840
|
+
return brain;
|
|
2841
|
+
}, "factory"),
|
|
2842
|
+
onError: /* @__PURE__ */ __name((repo, err) => {
|
|
2843
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2844
|
+
console.error(c.red(` Pool error [${repo}]: ${msg}`));
|
|
2845
|
+
}, "onError"),
|
|
2846
|
+
onLog: /* @__PURE__ */ __name((msg) => console.log(c.dim(` ${msg}`)), "onLog")
|
|
2847
|
+
});
|
|
2848
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank HTTP Daemon \u2501\u2501\u2501\n"));
|
|
2849
|
+
await server.start();
|
|
2850
|
+
console.log(c.dim(` Port: ${port}`));
|
|
2851
|
+
console.log(c.dim(" Press Ctrl+C to stop.\n"));
|
|
2852
|
+
const shutdown = /* @__PURE__ */ __name(() => {
|
|
2853
|
+
console.log(c.dim("\n Shutting down..."));
|
|
2854
|
+
server.close();
|
|
2855
|
+
process.exit(0);
|
|
2856
|
+
}, "shutdown");
|
|
2857
|
+
process.on("SIGINT", shutdown);
|
|
2858
|
+
process.on("SIGTERM", shutdown);
|
|
2859
|
+
await new Promise(() => {
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
__name(startForeground, "startForeground");
|
|
2863
|
+
async function forkDaemon() {
|
|
2864
|
+
const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
|
|
2865
|
+
const { fork } = await import("child_process");
|
|
2866
|
+
const existing = isServerRunning();
|
|
2867
|
+
if (existing) {
|
|
2868
|
+
console.log(c.yellow(` Daemon already running (PID ${existing.pid}, port ${existing.port})`));
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const child = fork(process.argv[1], ["daemon", "--port", String(port)], {
|
|
2872
|
+
detached: true,
|
|
2873
|
+
stdio: "ignore"
|
|
2874
|
+
});
|
|
2875
|
+
child.unref();
|
|
2876
|
+
console.log(c.green(` \u2713 Daemon started (PID ${child.pid}, port ${port})`));
|
|
2877
|
+
console.log(c.dim(" Stop with: brainbank daemon stop"));
|
|
2878
|
+
}
|
|
2879
|
+
__name(forkDaemon, "forkDaemon");
|
|
2880
|
+
function stopDaemon() {
|
|
2881
|
+
const info = isServerRunning();
|
|
2882
|
+
if (!info) {
|
|
2883
|
+
console.log(c.yellow(" No daemon running."));
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
process.kill(info.pid, "SIGTERM");
|
|
2888
|
+
removePid();
|
|
2889
|
+
console.log(c.green(` \u2713 Daemon stopped (PID ${info.pid})`));
|
|
2890
|
+
} catch {
|
|
2891
|
+
removePid();
|
|
2892
|
+
console.log(c.yellow(` PID ${info.pid} not found. Cleaned up stale PID file.`));
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
__name(stopDaemon, "stopDaemon");
|
|
2896
|
+
|
|
2897
|
+
// src/cli/commands/status.ts
|
|
2898
|
+
function formatUptime(seconds) {
|
|
2899
|
+
if (seconds < 60) return `${seconds}s`;
|
|
2900
|
+
const minutes = Math.floor(seconds / 60);
|
|
2901
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2902
|
+
const hours = Math.floor(minutes / 60);
|
|
2903
|
+
const remainMinutes = minutes % 60;
|
|
2904
|
+
return remainMinutes > 0 ? `${hours}h ${remainMinutes}m` : `${hours}h`;
|
|
2905
|
+
}
|
|
2906
|
+
__name(formatUptime, "formatUptime");
|
|
2907
|
+
async function cmdStatus() {
|
|
2908
|
+
const info = isServerRunning();
|
|
2909
|
+
if (!info) {
|
|
2910
|
+
console.log(`
|
|
2911
|
+
${c.dim("HTTP Server:")} ${c.yellow("stopped")}
|
|
2912
|
+
`);
|
|
2913
|
+
console.log(c.dim(" Start with: brainbank daemon"));
|
|
2914
|
+
console.log("");
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
const health = await serverHealth();
|
|
2918
|
+
if (health) {
|
|
2919
|
+
const uptime = formatUptime(health.uptime);
|
|
2920
|
+
console.log(`
|
|
2921
|
+
${c.dim("HTTP Server:")} ${c.green("running")}`);
|
|
2922
|
+
console.log(` ${c.dim("PID:")} ${health.pid}`);
|
|
2923
|
+
console.log(` ${c.dim("Port:")} ${health.port}`);
|
|
2924
|
+
console.log(` ${c.dim("Uptime:")} ${uptime}`);
|
|
2925
|
+
console.log(` ${c.dim("Workspaces:")} ${health.workspaces}`);
|
|
2926
|
+
console.log("");
|
|
2927
|
+
} else {
|
|
2928
|
+
console.log(`
|
|
2929
|
+
${c.dim("HTTP Server:")} ${c.yellow("stale")} (PID ${info.pid} not responding)`);
|
|
2930
|
+
console.log(c.dim(" The PID file may be stale. Restart with: brainbank daemon"));
|
|
2931
|
+
console.log("");
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
__name(cmdStatus, "cmdStatus");
|
|
2935
|
+
|
|
2936
|
+
// src/cli/commands/help.ts
|
|
2937
|
+
function showHelp() {
|
|
2938
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2014 Semantic Knowledge Bank \u2501\u2501\u2501\n"));
|
|
2939
|
+
console.log(c.bold("Indexing:"));
|
|
2940
|
+
console.log(` ${c.cyan("index")} ${c.dim("(i)")} [path] Index code + git history`);
|
|
2941
|
+
console.log(` ${c.cyan("collection add")} <path> --name Add a document collection`);
|
|
2942
|
+
console.log(` ${c.cyan("collection list")} List collections`);
|
|
2943
|
+
console.log(` ${c.cyan("collection remove")} <name> Remove a collection`);
|
|
2944
|
+
console.log(` ${c.cyan("docs")} [--collection <name>] Index document collections`);
|
|
2945
|
+
console.log("");
|
|
2946
|
+
console.log(c.bold("Search:"));
|
|
2947
|
+
console.log(` ${c.cyan("search")} <query> Semantic search (vector)`);
|
|
2948
|
+
console.log(` ${c.cyan("hsearch")} <query> Hybrid search (${c.bold("best quality")})`);
|
|
2949
|
+
console.log(` ${c.cyan("ksearch")} <query> Keyword search (BM25, instant)`);
|
|
2950
|
+
console.log(` ${c.cyan("dsearch")} <query> Document search`);
|
|
2951
|
+
console.log(c.dim(" All accept --repo <path> --path <dir> --<source> <n>"));
|
|
2952
|
+
console.log("");
|
|
2953
|
+
console.log(c.bold("Context:"));
|
|
2954
|
+
console.log(` ${c.cyan("context")} <task> Get formatted context for a task`);
|
|
2955
|
+
console.log(` ${c.cyan("context add")} <col> <path> <desc> Add context metadata`);
|
|
2956
|
+
console.log(` ${c.cyan("context list")} List all context metadata`);
|
|
2957
|
+
console.log(` ${c.cyan("files")} <path|glob> [...] [--lines] View full indexed files directly`);
|
|
2958
|
+
console.log("");
|
|
2959
|
+
console.log(c.bold("KV Store:"));
|
|
2960
|
+
console.log(` ${c.cyan("kv add")} <coll> <content> Add item to a collection`);
|
|
2961
|
+
console.log(` ${c.cyan("kv search")} <coll> <query> Search a collection`);
|
|
2962
|
+
console.log(` ${c.cyan("kv list")} [coll] List collections or items`);
|
|
2963
|
+
console.log(` ${c.cyan("kv trim")} <coll> --keep <n> Keep only N most recent`);
|
|
2964
|
+
console.log(` ${c.cyan("kv clear")} <coll> Clear all items`);
|
|
2965
|
+
console.log("");
|
|
2966
|
+
console.log(c.bold("Utility:"));
|
|
2967
|
+
console.log(` ${c.cyan("stats")} Show index statistics`);
|
|
2968
|
+
console.log(` ${c.cyan("reembed")} Re-embed all vectors`);
|
|
2969
|
+
console.log(` ${c.cyan("watch")} Watch files, auto-re-index`);
|
|
2970
|
+
console.log(` ${c.cyan("mcp")} Start MCP server (stdio)`);
|
|
2971
|
+
console.log(` ${c.cyan("mcp:export")} [target] Export MCP config (antigravity)`);
|
|
2972
|
+
console.log(` ${c.cyan("daemon")} Start HTTP daemon (foreground)`);
|
|
2973
|
+
console.log(` ${c.cyan("daemon start")} Start HTTP daemon (background)`);
|
|
2974
|
+
console.log(` ${c.cyan("daemon stop")} Stop background daemon`);
|
|
2975
|
+
console.log(` ${c.cyan("daemon restart")} Restart background daemon`);
|
|
2976
|
+
console.log(` ${c.cyan("status")} Show daemon status`);
|
|
2977
|
+
console.log(` ${c.cyan("--version")} ${c.dim("(-v)")} Show version`);
|
|
2978
|
+
console.log("");
|
|
2979
|
+
console.log(c.bold("Options:"));
|
|
2980
|
+
console.log(` ${c.dim("--repo <path>")} Repository path (default: .)`);
|
|
2981
|
+
console.log(` ${c.dim("--force")} Force re-index all files`);
|
|
2982
|
+
console.log(` ${c.dim("--depth <n>")} Git history depth (default: 500)`);
|
|
2983
|
+
console.log(` ${c.dim("--collection <name>")} Filter by collection`);
|
|
2984
|
+
console.log(` ${c.dim("--pattern <glob>")} Collection glob (default: **/*.md)`);
|
|
2985
|
+
console.log(` ${c.dim("--context <desc>")} Context description`);
|
|
2986
|
+
console.log(` ${c.dim("--<source> <n>")} Source filter: max results from <source> (0 = skip)`);
|
|
2987
|
+
console.log(` ${c.dim("--path <dir>")} Filter context results to files under this path prefix`);
|
|
2988
|
+
console.log(` ${c.dim("--ignore <globs>")} Ignore glob patterns for code indexing (comma-separated)`);
|
|
2989
|
+
console.log(` ${c.dim("--include <globs>")} Include only these paths for code indexing (comma-separated)`);
|
|
2990
|
+
console.log(` ${c.dim("--yes / -y")} Skip interactive prompt (auto-select all available)`);
|
|
2991
|
+
console.log(` ${c.dim("--setup")} Re-run interactive setup (modules, folders, config)`);
|
|
2992
|
+
console.log(` ${c.dim("--port <n>")} HTTP daemon port (default: 8181)`);
|
|
2993
|
+
console.log("");
|
|
2994
|
+
console.log(c.bold("Examples:"));
|
|
2995
|
+
console.log(c.dim(" brainbank index ."));
|
|
2996
|
+
console.log(c.dim(' brainbank index . --ignore "sdk/**,vendor/**"'));
|
|
2997
|
+
console.log(c.dim(' brainbank index . --include "src/**,lib/**"'));
|
|
2998
|
+
console.log(c.dim(' brainbank kv add errors "Fixed null pointer in api.ts"'));
|
|
2999
|
+
console.log(c.dim(' brainbank kv search errors "null pointer"'));
|
|
3000
|
+
console.log(c.dim(" brainbank kv list"));
|
|
3001
|
+
console.log(c.dim(' brainbank hsearch "authentication middleware"'));
|
|
3002
|
+
console.log(c.dim(' brainbank hsearch "auth" --code 0 --git 10 # git only'));
|
|
3003
|
+
console.log(c.dim(' brainbank search "handler" --git 0 # code only'));
|
|
3004
|
+
console.log(c.dim(' brainbank hsearch "api" --docs 10 --code 0 --git 0 # docs only'));
|
|
3005
|
+
console.log(c.dim(' brainbank context "auth flow" | pbcopy # \u2192 clipboard'));
|
|
3006
|
+
console.log(c.dim(" brainbank daemon start # background HTTP"));
|
|
3007
|
+
console.log(c.dim(" brainbank mcp # MCP stdio"));
|
|
3008
|
+
console.log(c.dim(" brainbank mcp:export antigravity # export MCP config"));
|
|
3009
|
+
}
|
|
3010
|
+
__name(showHelp, "showHelp");
|
|
3011
|
+
|
|
3012
|
+
// src/cli/index.ts
|
|
3013
|
+
var command = args[0];
|
|
3014
|
+
async function main() {
|
|
3015
|
+
switch (command) {
|
|
3016
|
+
case "--version":
|
|
3017
|
+
case "-v":
|
|
3018
|
+
console.log(`brainbank v${VERSION}`);
|
|
3019
|
+
break;
|
|
3020
|
+
case "i":
|
|
3021
|
+
case "index":
|
|
3022
|
+
return cmdIndex();
|
|
3023
|
+
case "collection":
|
|
3024
|
+
return cmdCollection();
|
|
3025
|
+
case "kv":
|
|
3026
|
+
return cmdKv();
|
|
3027
|
+
case "docs":
|
|
3028
|
+
return cmdDocs();
|
|
3029
|
+
case "dsearch":
|
|
3030
|
+
return cmdDocSearch();
|
|
3031
|
+
case "search":
|
|
3032
|
+
return cmdSearch();
|
|
3033
|
+
case "hsearch":
|
|
3034
|
+
return cmdHybridSearch();
|
|
3035
|
+
case "ksearch":
|
|
3036
|
+
return cmdKeywordSearch();
|
|
3037
|
+
case "context":
|
|
3038
|
+
return cmdContext();
|
|
3039
|
+
case "files":
|
|
3040
|
+
return cmdFiles();
|
|
3041
|
+
case "stats":
|
|
3042
|
+
return cmdStats();
|
|
3043
|
+
case "reembed":
|
|
3044
|
+
return cmdReembed();
|
|
3045
|
+
case "watch":
|
|
3046
|
+
return cmdWatch();
|
|
3047
|
+
case "mcp":
|
|
3048
|
+
return cmdMcp();
|
|
3049
|
+
case "mcp:export":
|
|
3050
|
+
return cmdMcpExport();
|
|
3051
|
+
case "serve":
|
|
3052
|
+
return cmdMcp();
|
|
3053
|
+
// backward compat
|
|
3054
|
+
case "daemon":
|
|
3055
|
+
return cmdDaemon();
|
|
3056
|
+
case "status":
|
|
3057
|
+
return cmdStatus();
|
|
3058
|
+
case "help":
|
|
3059
|
+
case "--help":
|
|
3060
|
+
case "-h":
|
|
3061
|
+
showHelp();
|
|
3062
|
+
break;
|
|
3063
|
+
default:
|
|
3064
|
+
if (command) console.log(c.red(`Unknown command: ${command}
|
|
3065
|
+
`));
|
|
3066
|
+
showHelp();
|
|
3067
|
+
process.exit(command ? 1 : 0);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
__name(main, "main");
|
|
3071
|
+
main().catch((err) => {
|
|
3072
|
+
console.error(c.red(`Error: ${err.message}`));
|
|
3073
|
+
if (process.env.BRAINBANK_DEBUG) console.error(err.stack);
|
|
3074
|
+
process.exit(1);
|
|
3075
|
+
});
|
|
3076
|
+
//# sourceMappingURL=cli.js.map
|