brainbank 0.1.3 → 0.1.4
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/README.md +84 -1107
- package/assets/architecture.png +0 -0
- package/bin/brainbank +8 -1
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3UIWA32X.js +3341 -0
- package/dist/chunk-3UIWA32X.js.map +1 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-DAGVUEXL.js +258 -0
- package/dist/chunk-DAGVUEXL.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-NNDY7P2R.js +211 -0
- package/dist/chunk-NNDY7P2R.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/cli.js +2713 -325
- package/dist/cli.js.map +1 -1
- package/dist/haiku-pruner-5KVT5AI2.js +8 -0
- package/dist/http-server-2ZQ6I43B.js +9 -0
- package/dist/index.d.ts +1886 -626
- package/dist/index.js +319 -46
- package/dist/index.js.map +1 -1
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +386 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -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-AD3AMYGV.js +1904 -0
- package/dist/stats-tui-AD3AMYGV.js.map +1 -0
- package/package.json +38 -53
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +59 -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 +82 -0
- package/src/cli/commands/index.ts +478 -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/query.ts +167 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/reindex.ts +40 -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 +324 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/server-client.ts +186 -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 +48 -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 +150 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +179 -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 +72 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +268 -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 +178 -0
- package/src/providers/pruners/haiku-pruner.ts +263 -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 +76 -0
- package/src/search/context-builder.ts +209 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/query-decomposer.ts +124 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +105 -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 +344 -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 +509 -0
- package/dist/chunk-2P3EGY6S.js +0 -37
- package/dist/chunk-2P3EGY6S.js.map +0 -1
- package/dist/chunk-3GAIDXRW.js +0 -105
- package/dist/chunk-3GAIDXRW.js.map +0 -1
- package/dist/chunk-4ZKBQ33J.js +0 -56
- package/dist/chunk-4ZKBQ33J.js.map +0 -1
- package/dist/chunk-7QVYU63E.js +0 -7
- package/dist/chunk-GOUBW7UA.js +0 -373
- package/dist/chunk-GOUBW7UA.js.map +0 -1
- package/dist/chunk-MJ3Y24H6.js +0 -185
- package/dist/chunk-MJ3Y24H6.js.map +0 -1
- package/dist/chunk-N6ZMBFDE.js +0 -224
- package/dist/chunk-N6ZMBFDE.js.map +0 -1
- package/dist/chunk-RAEBYV75.js +0 -709
- package/dist/chunk-RAEBYV75.js.map +0 -1
- package/dist/chunk-TW5NTYYZ.js +0 -2066
- package/dist/chunk-TW5NTYYZ.js.map +0 -1
- package/dist/chunk-Z5SU54HP.js +0 -171
- package/dist/chunk-Z5SU54HP.js.map +0 -1
- package/dist/code.d.ts +0 -31
- package/dist/code.js +0 -8
- package/dist/docs.d.ts +0 -19
- package/dist/docs.js +0 -8
- package/dist/git.d.ts +0 -31
- package/dist/git.js +0 -8
- package/dist/memory.d.ts +0 -19
- package/dist/memory.js +0 -146
- package/dist/memory.js.map +0 -1
- package/dist/notes.d.ts +0 -19
- package/dist/notes.js +0 -57
- package/dist/notes.js.map +0 -1
- package/dist/openai-PCTYLOWI.js +0 -8
- package/dist/types-Da_zLLOl.d.ts +0 -474
- /package/dist/{chunk-7QVYU63E.js.map → chunk-WCQVDF3K.js.map} +0 -0
- /package/dist/{code.js.map → haiku-pruner-5KVT5AI2.js.map} +0 -0
- /package/dist/{docs.js.map → http-server-2ZQ6I43B.js.map} +0 -0
- /package/dist/{git.js.map → local-embedding-NZQTILGV.js.map} +0 -0
- /package/dist/{openai-PCTYLOWI.js.map → openai-embedding-ZP5TSUJG.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,245 +1,1971 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
DEFAULT_PORT,
|
|
4
|
+
isServerRunning,
|
|
5
|
+
removePid
|
|
6
|
+
} from "./chunk-RDQYDLYZ.js";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
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-3UIWA32X.js";
|
|
27
|
+
import "./chunk-M744PCJQ.js";
|
|
28
|
+
import "./chunk-IMJJ2VEM.js";
|
|
8
29
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
docs
|
|
13
|
-
} from "./chunk-GOUBW7UA.js";
|
|
14
|
-
import "./chunk-4ZKBQ33J.js";
|
|
15
|
-
import "./chunk-2P3EGY6S.js";
|
|
16
|
-
import {
|
|
17
|
-
__name
|
|
18
|
-
} from "./chunk-7QVYU63E.js";
|
|
30
|
+
__name,
|
|
31
|
+
__require
|
|
32
|
+
} from "./chunk-WCQVDF3K.js";
|
|
19
33
|
|
|
20
|
-
// src/
|
|
21
|
-
import * as
|
|
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
|
|
22
39
|
import * as fs from "fs";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
49
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var _folderIndexersCache = void 0;
|
|
52
|
-
async function loadConfig() {
|
|
53
|
-
if (_configCache !== void 0) return _configCache;
|
|
54
|
-
const repoPath = getFlag("repo") ?? ".";
|
|
55
|
-
const brainbankDir = path.resolve(repoPath, ".brainbank");
|
|
56
|
-
for (const name of CONFIG_NAMES) {
|
|
57
|
-
const configPath = path.join(brainbankDir, name);
|
|
58
|
-
if (fs.existsSync(configPath)) {
|
|
59
|
-
try {
|
|
60
|
-
const mod = await import(configPath);
|
|
61
|
-
_configCache = mod.default ?? mod;
|
|
62
|
-
return _configCache;
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error(c.red(`Error loading .brainbank/${name}: ${err.message}`));
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
_configCache = null;
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
__name(loadConfig, "loadConfig");
|
|
73
|
-
async function discoverFolderIndexers() {
|
|
74
|
-
if (_folderIndexersCache !== void 0) return _folderIndexersCache;
|
|
75
|
-
const repoPath = getFlag("repo") ?? ".";
|
|
76
|
-
const indexersDir = path.resolve(repoPath, ".brainbank", "indexers");
|
|
77
|
-
if (!fs.existsSync(indexersDir)) {
|
|
78
|
-
_folderIndexersCache = [];
|
|
79
|
-
return [];
|
|
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;
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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) {
|
|
85
77
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
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")}`);
|
|
90
214
|
} else {
|
|
91
|
-
console.
|
|
92
|
-
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error(c.red(`Error loading indexer ${file}: ${err.message}`));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
_folderIndexersCache = indexers;
|
|
98
|
-
return indexers;
|
|
99
|
-
}
|
|
100
|
-
__name(discoverFolderIndexers, "discoverFolderIndexers");
|
|
101
|
-
async function createBrain(repoPath) {
|
|
102
|
-
const rp = repoPath ?? getFlag("repo") ?? ".";
|
|
103
|
-
const config = await loadConfig();
|
|
104
|
-
const folderIndexers = await discoverFolderIndexers();
|
|
105
|
-
const brainOpts = { repoPath: rp, ...config?.brainbank ?? {} };
|
|
106
|
-
const rerankerFlag = getFlag("reranker");
|
|
107
|
-
if (rerankerFlag === "qwen3") {
|
|
108
|
-
const { Qwen3Reranker } = await import("@brainbank/reranker");
|
|
109
|
-
brainOpts.reranker = new Qwen3Reranker();
|
|
110
|
-
}
|
|
111
|
-
const embeddingEnv = process.env.BRAINBANK_EMBEDDING;
|
|
112
|
-
if (embeddingEnv === "openai") {
|
|
113
|
-
const { OpenAIEmbedding } = await import("./openai-PCTYLOWI.js");
|
|
114
|
-
const provider = new OpenAIEmbedding();
|
|
115
|
-
brainOpts.embeddingProvider = provider;
|
|
116
|
-
brainOpts.embeddingDims = provider.dims;
|
|
117
|
-
}
|
|
118
|
-
const brain = new BrainBank(brainOpts);
|
|
119
|
-
const builtins = config?.builtins ?? ["code", "git", "docs"];
|
|
120
|
-
const resolvedRp = path.resolve(rp);
|
|
121
|
-
const hasRootGit = fs.existsSync(path.join(resolvedRp, ".git"));
|
|
122
|
-
const gitSubdirs = !hasRootGit ? detectGitSubdirs(resolvedRp) : [];
|
|
123
|
-
if (gitSubdirs.length > 0 && (builtins.includes("code") || builtins.includes("git"))) {
|
|
124
|
-
console.log(c.cyan(` Multi-repo: found ${gitSubdirs.length} git repos: ${gitSubdirs.map((d) => d.name).join(", ")}`));
|
|
125
|
-
for (const sub of gitSubdirs) {
|
|
126
|
-
if (builtins.includes("code")) {
|
|
127
|
-
brain.use(code({ repoPath: sub.path, name: `code:${sub.name}` }));
|
|
128
|
-
}
|
|
129
|
-
if (builtins.includes("git")) {
|
|
130
|
-
brain.use(git({ repoPath: sub.path, name: `git:${sub.name}` }));
|
|
215
|
+
console.log(` ${c.dim("GEMINI.md \u2014 skipped")}`);
|
|
131
216
|
}
|
|
132
217
|
}
|
|
133
218
|
} else {
|
|
134
|
-
if (
|
|
135
|
-
|
|
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 };
|
|
136
716
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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: [] };
|
|
140
747
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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}/**`);
|
|
144
800
|
}
|
|
145
801
|
}
|
|
146
|
-
|
|
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 };
|
|
147
851
|
}
|
|
148
|
-
__name(
|
|
149
|
-
function
|
|
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) {
|
|
150
872
|
try {
|
|
151
|
-
|
|
152
|
-
return entries.filter(
|
|
153
|
-
(e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("node_modules") && fs.existsSync(path.join(parentPath, e.name, ".git"))
|
|
154
|
-
).map((e) => ({ name: e.name, path: path.join(parentPath, e.name) }));
|
|
873
|
+
return fs3.readdirSync(dir, { withFileTypes: true });
|
|
155
874
|
} catch {
|
|
156
875
|
return [];
|
|
157
876
|
}
|
|
158
877
|
}
|
|
159
|
-
__name(
|
|
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
|
|
160
1577
|
async function cmdIndex() {
|
|
161
|
-
const
|
|
1578
|
+
const positional = stripFlags(args);
|
|
1579
|
+
const repoPath = positional[1] || ".";
|
|
162
1580
|
const force = hasFlag("force");
|
|
163
1581
|
const depth = parseInt(getFlag("depth") || "500", 10);
|
|
164
1582
|
const onlyRaw = getFlag("only");
|
|
165
1583
|
const docsPath = getFlag("docs");
|
|
166
|
-
const
|
|
167
|
-
|
|
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")) {
|
|
168
1644
|
modules.push("docs");
|
|
169
1645
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
1646
|
+
if (tuiConfig) {
|
|
1647
|
+
saveConfigFromTui(scan.repoPath, modules, tuiConfig.embedding, tuiConfig.pruner, 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, 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 (include.length > 0) {
|
|
1817
|
+
config.include = include;
|
|
1818
|
+
}
|
|
1819
|
+
if (ignore.length > 0) {
|
|
1820
|
+
config.ignore = ignore;
|
|
1821
|
+
}
|
|
1822
|
+
if (modules.includes("docs")) {
|
|
1823
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
1824
|
+
if (collections.length > 0) {
|
|
1825
|
+
config.docs = { collections };
|
|
1826
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map((dc) => dc.name).join(", ")}`));
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const detectedKeys = {};
|
|
1830
|
+
const needsPerplexity = embedding.startsWith("perplexity");
|
|
1831
|
+
const needsAnthropic = pruner === "haiku";
|
|
1832
|
+
const needsOpenai = embedding === "openai";
|
|
1833
|
+
if (needsPerplexity && process.env.PERPLEXITY_API_KEY) {
|
|
1834
|
+
detectedKeys.perplexity = process.env.PERPLEXITY_API_KEY;
|
|
1835
|
+
}
|
|
1836
|
+
if (needsAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
1837
|
+
detectedKeys.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
1838
|
+
}
|
|
1839
|
+
if (needsOpenai && process.env.OPENAI_API_KEY) {
|
|
1840
|
+
detectedKeys.openai = process.env.OPENAI_API_KEY;
|
|
1841
|
+
}
|
|
1842
|
+
if (Object.keys(detectedKeys).length > 0) {
|
|
1843
|
+
config.keys = detectedKeys;
|
|
1844
|
+
}
|
|
1845
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
1846
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1847
|
+
console.log(c.green(` \u2713 Saved ${path4.relative(process.cwd(), configPath)}`));
|
|
1848
|
+
}
|
|
1849
|
+
__name(saveConfigFromTui, "saveConfigFromTui");
|
|
1850
|
+
function updateConfigPlugins(repoPath, modules, include, ignore) {
|
|
1851
|
+
const configPath = path4.join(repoPath, ".brainbank", "config.json");
|
|
1852
|
+
try {
|
|
1853
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
1854
|
+
const config = JSON.parse(raw);
|
|
1855
|
+
config.plugins = modules;
|
|
1856
|
+
if (include.length > 0) {
|
|
1857
|
+
config.include = include;
|
|
1858
|
+
} else {
|
|
1859
|
+
delete config.include;
|
|
1860
|
+
}
|
|
1861
|
+
if (ignore.length > 0) {
|
|
1862
|
+
config.ignore = ignore;
|
|
1863
|
+
} else {
|
|
1864
|
+
delete config.ignore;
|
|
1865
|
+
}
|
|
1866
|
+
if (modules.includes("docs")) {
|
|
1867
|
+
const existing = config.docs;
|
|
1868
|
+
const hasCollections = existing && Array.isArray(existing.collections) && existing.collections.length > 0;
|
|
1869
|
+
if (!hasCollections) {
|
|
1870
|
+
const collections = autoDetectDocCollections(repoPath);
|
|
1871
|
+
if (collections.length > 0) {
|
|
1872
|
+
config.docs = { ...existing ?? {}, collections };
|
|
1873
|
+
console.log(c.dim(` Auto-detected docs: ${collections.map((dc) => dc.name).join(", ")}`));
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1878
|
+
console.log(c.green(` \u2713 Updated config.json`));
|
|
1879
|
+
} catch {
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
__name(updateConfigPlugins, "updateConfigPlugins");
|
|
1883
|
+
function deindexModule(repoPath, moduleName) {
|
|
1884
|
+
const dbPath = path4.join(repoPath, ".brainbank", "data", "brainbank.db");
|
|
1885
|
+
if (!fs4.existsSync(dbPath)) return;
|
|
1886
|
+
let db;
|
|
1887
|
+
try {
|
|
1888
|
+
const sqlite = __require("sqlite");
|
|
1889
|
+
db = new sqlite.DatabaseSync(dbPath);
|
|
1890
|
+
} catch {
|
|
1891
|
+
console.log(c.yellow(` Could not open DB \u2014 skip deindex for ${moduleName}`));
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (!db) return;
|
|
1895
|
+
const tables = {
|
|
1896
|
+
code: [
|
|
1897
|
+
"DELETE FROM code_call_edges",
|
|
1898
|
+
"DELETE FROM code_refs",
|
|
1899
|
+
"DELETE FROM code_symbols",
|
|
1900
|
+
"DELETE FROM code_imports",
|
|
1901
|
+
"DELETE FROM code_vectors",
|
|
1902
|
+
"DELETE FROM code_chunks",
|
|
1903
|
+
"DELETE FROM indexed_files",
|
|
1904
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'code'"
|
|
1905
|
+
],
|
|
1906
|
+
docs: [
|
|
1907
|
+
"DELETE FROM doc_vectors",
|
|
1908
|
+
"DELETE FROM doc_chunks",
|
|
1909
|
+
"DELETE FROM path_contexts",
|
|
1910
|
+
"DELETE FROM collections",
|
|
1911
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'docs'"
|
|
1912
|
+
],
|
|
1913
|
+
git: [
|
|
1914
|
+
"DELETE FROM git_vectors",
|
|
1915
|
+
"DELETE FROM git_commits",
|
|
1916
|
+
"DELETE FROM plugin_tracking WHERE plugin = 'git'"
|
|
1917
|
+
]
|
|
1918
|
+
};
|
|
1919
|
+
const statements = tables[moduleName];
|
|
1920
|
+
if (!statements) {
|
|
1921
|
+
console.log(c.dim(` No known tables for ${moduleName}`));
|
|
180
1922
|
try {
|
|
181
|
-
|
|
182
|
-
name: collName,
|
|
183
|
-
path: absDocsPath,
|
|
184
|
-
pattern: "**/*.md",
|
|
185
|
-
ignore: ["deprecated/**", "node_modules/**"]
|
|
186
|
-
});
|
|
187
|
-
console.log(c.dim(` Registered docs collection: ${collName}`));
|
|
1923
|
+
db.close();
|
|
188
1924
|
} catch {
|
|
189
|
-
console.log(c.yellow(` Warning: docs module not loaded, skipping --docs`));
|
|
190
1925
|
}
|
|
1926
|
+
return;
|
|
191
1927
|
}
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
onProgress: /* @__PURE__ */ __name((stage, msg) => {
|
|
197
|
-
process.stdout.write(`\r ${c.cyan(stage.toUpperCase())} ${msg} `);
|
|
198
|
-
}, "onProgress")
|
|
199
|
-
});
|
|
200
|
-
console.log("\n");
|
|
201
|
-
if (result.code) {
|
|
202
|
-
console.log(` ${c.green("Code")}: ${result.code.indexed} indexed, ${result.code.skipped} skipped, ${result.code.chunks ?? 0} chunks`);
|
|
203
|
-
}
|
|
204
|
-
if (result.git) {
|
|
205
|
-
console.log(` ${c.green("Git")}: ${result.git.indexed} indexed, ${result.git.skipped} skipped`);
|
|
206
|
-
}
|
|
207
|
-
if (result.docs) {
|
|
208
|
-
for (const [name, stat] of Object.entries(result.docs)) {
|
|
209
|
-
console.log(` ${c.green("Docs")}: [${name}] ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
|
|
1928
|
+
for (const sql of statements) {
|
|
1929
|
+
try {
|
|
1930
|
+
db.exec(sql);
|
|
1931
|
+
} catch {
|
|
210
1932
|
}
|
|
211
1933
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (stats.git) console.log(` Git commits: ${stats.git.commits}`);
|
|
217
|
-
if (stats.git) console.log(` Co-edit pairs: ${stats.git.coEdits}`);
|
|
218
|
-
if (stats.documents) console.log(` Documents: ${stats.documents.documents}`);
|
|
219
|
-
brain.close();
|
|
1934
|
+
try {
|
|
1935
|
+
db.close();
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
220
1938
|
}
|
|
221
|
-
__name(
|
|
1939
|
+
__name(deindexModule, "deindexModule");
|
|
1940
|
+
|
|
1941
|
+
// src/cli/commands/collection.ts
|
|
222
1942
|
async function cmdCollection() {
|
|
223
|
-
const
|
|
1943
|
+
const pos = stripFlags(args);
|
|
1944
|
+
const sub = pos[1];
|
|
224
1945
|
if (sub === "add") {
|
|
225
|
-
const
|
|
1946
|
+
const path7 = pos[2];
|
|
226
1947
|
const name = getFlag("name");
|
|
227
1948
|
const pattern = getFlag("pattern") ?? "**/*.md";
|
|
228
1949
|
const context = getFlag("context");
|
|
229
1950
|
const ignoreRaw = getFlag("ignore");
|
|
230
|
-
if (!
|
|
1951
|
+
if (!path7 || !name) {
|
|
231
1952
|
console.log(c.red('Usage: brainbank collection add <path> --name <name> [--pattern "**/*.md"] [--ignore "glob"] [--context "description"]'));
|
|
232
1953
|
process.exit(1);
|
|
233
1954
|
}
|
|
234
1955
|
const brain = await createBrain();
|
|
235
|
-
|
|
1956
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
1957
|
+
if (!docsPlugin) {
|
|
1958
|
+
console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
await docsPlugin.addCollection({
|
|
236
1962
|
name,
|
|
237
|
-
path:
|
|
1963
|
+
path: path7,
|
|
238
1964
|
pattern,
|
|
239
1965
|
ignore: ignoreRaw ? ignoreRaw.split(",") : [],
|
|
240
1966
|
context: context ?? void 0
|
|
241
1967
|
});
|
|
242
|
-
console.log(c.green(`\u2713 Collection '${name}' added: ${
|
|
1968
|
+
console.log(c.green(`\u2713 Collection '${name}' added: ${path7} (${pattern})`));
|
|
243
1969
|
if (context) console.log(c.dim(` Context: ${context}`));
|
|
244
1970
|
brain.close();
|
|
245
1971
|
return;
|
|
@@ -247,7 +1973,13 @@ async function cmdCollection() {
|
|
|
247
1973
|
if (sub === "list") {
|
|
248
1974
|
const brain = await createBrain();
|
|
249
1975
|
await brain.initialize();
|
|
250
|
-
const
|
|
1976
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
1977
|
+
if (!docsPlugin) {
|
|
1978
|
+
console.log(c.yellow(" Docs plugin not loaded."));
|
|
1979
|
+
brain.close();
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const collections = docsPlugin.listCollections();
|
|
251
1983
|
if (collections.length === 0) {
|
|
252
1984
|
console.log(c.yellow(" No collections registered."));
|
|
253
1985
|
} else {
|
|
@@ -262,13 +1994,18 @@ async function cmdCollection() {
|
|
|
262
1994
|
return;
|
|
263
1995
|
}
|
|
264
1996
|
if (sub === "remove") {
|
|
265
|
-
const name =
|
|
1997
|
+
const name = pos[2];
|
|
266
1998
|
if (!name) {
|
|
267
1999
|
console.log(c.red("Usage: brainbank collection remove <name>"));
|
|
268
2000
|
process.exit(1);
|
|
269
2001
|
}
|
|
270
2002
|
const brain = await createBrain();
|
|
271
|
-
|
|
2003
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2004
|
+
if (!docsPlugin) {
|
|
2005
|
+
console.log(c.red("Docs plugin not loaded."));
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
await docsPlugin.removeCollection(name);
|
|
272
2009
|
console.log(c.green(`\u2713 Collection '${name}' removed.`));
|
|
273
2010
|
brain.close();
|
|
274
2011
|
return;
|
|
@@ -277,11 +2014,14 @@ async function cmdCollection() {
|
|
|
277
2014
|
process.exit(1);
|
|
278
2015
|
}
|
|
279
2016
|
__name(cmdCollection, "cmdCollection");
|
|
2017
|
+
|
|
2018
|
+
// src/cli/commands/kv.ts
|
|
280
2019
|
async function cmdKv() {
|
|
281
|
-
const
|
|
2020
|
+
const pos = stripFlags(args);
|
|
2021
|
+
const sub = pos[1];
|
|
282
2022
|
if (sub === "add") {
|
|
283
|
-
const collName =
|
|
284
|
-
const content =
|
|
2023
|
+
const collName = pos[2];
|
|
2024
|
+
const content = pos.slice(3).join(" ");
|
|
285
2025
|
const metaRaw = getFlag("meta");
|
|
286
2026
|
if (!collName || !content) {
|
|
287
2027
|
console.log(c.red(`Usage: brainbank kv add <collection> <content> [--meta '{"key":"val"}']`));
|
|
@@ -297,8 +2037,8 @@ async function cmdKv() {
|
|
|
297
2037
|
return;
|
|
298
2038
|
}
|
|
299
2039
|
if (sub === "search") {
|
|
300
|
-
const collName =
|
|
301
|
-
const query =
|
|
2040
|
+
const collName = pos[2];
|
|
2041
|
+
const query = pos.slice(3).join(" ");
|
|
302
2042
|
const k = parseInt(getFlag("k") || "5", 10);
|
|
303
2043
|
const mode = getFlag("mode") || "hybrid";
|
|
304
2044
|
if (!collName || !query) {
|
|
@@ -327,7 +2067,7 @@ async function cmdKv() {
|
|
|
327
2067
|
return;
|
|
328
2068
|
}
|
|
329
2069
|
if (sub === "list") {
|
|
330
|
-
const collName =
|
|
2070
|
+
const collName = pos[2];
|
|
331
2071
|
const limit = parseInt(getFlag("limit") || "20", 10);
|
|
332
2072
|
if (!collName) {
|
|
333
2073
|
const brain2 = await createBrain();
|
|
@@ -364,7 +2104,7 @@ async function cmdKv() {
|
|
|
364
2104
|
return;
|
|
365
2105
|
}
|
|
366
2106
|
if (sub === "trim") {
|
|
367
|
-
const collName =
|
|
2107
|
+
const collName = pos[2];
|
|
368
2108
|
const keep = parseInt(getFlag("keep") || "0", 10);
|
|
369
2109
|
if (!collName || keep <= 0) {
|
|
370
2110
|
console.log(c.red("Usage: brainbank kv trim <collection> --keep <n>"));
|
|
@@ -379,7 +2119,7 @@ async function cmdKv() {
|
|
|
379
2119
|
return;
|
|
380
2120
|
}
|
|
381
2121
|
if (sub === "clear") {
|
|
382
|
-
const collName =
|
|
2122
|
+
const collName = pos[2];
|
|
383
2123
|
if (!collName) {
|
|
384
2124
|
console.log(c.red("Usage: brainbank kv clear <collection>"));
|
|
385
2125
|
process.exit(1);
|
|
@@ -397,6 +2137,8 @@ async function cmdKv() {
|
|
|
397
2137
|
process.exit(1);
|
|
398
2138
|
}
|
|
399
2139
|
__name(cmdKv, "cmdKv");
|
|
2140
|
+
|
|
2141
|
+
// src/cli/commands/docs.ts
|
|
400
2142
|
async function cmdDocs() {
|
|
401
2143
|
const collection = getFlag("collection");
|
|
402
2144
|
const brain = await createBrain();
|
|
@@ -406,16 +2148,22 @@ async function cmdDocs() {
|
|
|
406
2148
|
opts.onProgress = (col, file, cur, total) => {
|
|
407
2149
|
process.stdout.write(`\r ${c.cyan(col)} [${cur}/${total}] ${file} `);
|
|
408
2150
|
};
|
|
409
|
-
const
|
|
2151
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2152
|
+
if (!docsPlugin) {
|
|
2153
|
+
console.log(c.red(" Docs plugin not loaded. Install @brainbank/docs."));
|
|
2154
|
+
process.exit(1);
|
|
2155
|
+
}
|
|
2156
|
+
const results = await docsPlugin.indexDocs(opts);
|
|
410
2157
|
console.log("\n");
|
|
411
2158
|
for (const [name, stat] of Object.entries(results)) {
|
|
412
|
-
|
|
2159
|
+
const removedStr = stat.removed > 0 ? `, ${c.red(String(stat.removed) + " removed")}` : "";
|
|
2160
|
+
console.log(` ${c.green(name)}: ${stat.indexed} indexed, ${stat.skipped} skipped${removedStr}, ${stat.chunks} chunks`);
|
|
413
2161
|
}
|
|
414
2162
|
brain.close();
|
|
415
2163
|
}
|
|
416
2164
|
__name(cmdDocs, "cmdDocs");
|
|
417
2165
|
async function cmdDocSearch() {
|
|
418
|
-
const query = args.slice(1).
|
|
2166
|
+
const query = stripFlags(args).slice(1).join(" ");
|
|
419
2167
|
if (!query) {
|
|
420
2168
|
console.log(c.red("Usage: brainbank dsearch <query>"));
|
|
421
2169
|
process.exit(1);
|
|
@@ -426,7 +2174,12 @@ async function cmdDocSearch() {
|
|
|
426
2174
|
console.log(c.bold(`
|
|
427
2175
|
\u2501\u2501\u2501 BrainBank Doc Search: "${query}" \u2501\u2501\u2501
|
|
428
2176
|
`));
|
|
429
|
-
const
|
|
2177
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2178
|
+
if (!docsPlugin) {
|
|
2179
|
+
console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
|
|
2180
|
+
process.exit(1);
|
|
2181
|
+
}
|
|
2182
|
+
const results = await docsPlugin.search(query, { collection: collection ?? void 0, k });
|
|
430
2183
|
if (results.length === 0) {
|
|
431
2184
|
console.log(c.yellow(" No results found."));
|
|
432
2185
|
brain.close();
|
|
@@ -435,7 +2188,7 @@ async function cmdDocSearch() {
|
|
|
435
2188
|
for (const r of results) {
|
|
436
2189
|
const score = Math.round(r.score * 100);
|
|
437
2190
|
const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
|
|
438
|
-
console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.metadata.collection}]${ctx}`);
|
|
2191
|
+
console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.type === "document" ? r.metadata.collection ?? "" : ""}]${ctx}`);
|
|
439
2192
|
const preview = r.content.split("\n").slice(0, 4).join("\n");
|
|
440
2193
|
console.log(c.dim(preview));
|
|
441
2194
|
console.log("");
|
|
@@ -443,155 +2196,576 @@ async function cmdDocSearch() {
|
|
|
443
2196
|
brain.close();
|
|
444
2197
|
}
|
|
445
2198
|
__name(cmdDocSearch, "cmdDocSearch");
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
2199
|
+
|
|
2200
|
+
// src/cli/server-client.ts
|
|
2201
|
+
import * as http from "http";
|
|
2202
|
+
async function tryServerContext(options) {
|
|
2203
|
+
const info = isServerRunning();
|
|
2204
|
+
if (!info) return null;
|
|
2205
|
+
try {
|
|
2206
|
+
const body = JSON.stringify({
|
|
2207
|
+
task: options.task,
|
|
2208
|
+
repo: options.repo,
|
|
2209
|
+
sources: options.sources,
|
|
2210
|
+
pathPrefix: options.pathPrefix,
|
|
2211
|
+
ignorePaths: options.ignorePaths,
|
|
2212
|
+
affectedFiles: options.affectedFiles,
|
|
2213
|
+
fields: options.fields,
|
|
2214
|
+
context: options.context,
|
|
2215
|
+
prunerContext: options.prunerContext
|
|
2216
|
+
});
|
|
2217
|
+
const response = await httpPost(info.port, "/context", body);
|
|
2218
|
+
const data = JSON.parse(response);
|
|
2219
|
+
if (data.error) return null;
|
|
2220
|
+
return data.context ?? null;
|
|
2221
|
+
} catch {
|
|
2222
|
+
return null;
|
|
462
2223
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
2224
|
+
}
|
|
2225
|
+
__name(tryServerContext, "tryServerContext");
|
|
2226
|
+
async function tryServerSearch(mode, options) {
|
|
2227
|
+
const info = isServerRunning();
|
|
2228
|
+
if (!info) return null;
|
|
2229
|
+
const endpoint = mode === "search" ? "/search" : mode === "hybrid" ? "/hsearch" : "/ksearch";
|
|
2230
|
+
try {
|
|
2231
|
+
const body = JSON.stringify({
|
|
2232
|
+
query: options.query,
|
|
2233
|
+
repo: options.repo,
|
|
2234
|
+
sources: options.sources,
|
|
2235
|
+
pathPrefix: options.pathPrefix,
|
|
2236
|
+
maxResults: options.maxResults
|
|
2237
|
+
});
|
|
2238
|
+
const response = await httpPost(info.port, endpoint, body);
|
|
2239
|
+
const data = JSON.parse(response);
|
|
2240
|
+
if (data.error) return null;
|
|
2241
|
+
return data.results ?? null;
|
|
2242
|
+
} catch {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
__name(tryServerSearch, "tryServerSearch");
|
|
2247
|
+
async function serverHealth() {
|
|
2248
|
+
const info = isServerRunning();
|
|
2249
|
+
if (!info) return null;
|
|
2250
|
+
try {
|
|
2251
|
+
const response = await httpGet(info.port, "/health");
|
|
2252
|
+
return JSON.parse(response);
|
|
2253
|
+
} catch {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
__name(serverHealth, "serverHealth");
|
|
2258
|
+
function httpPost(port, path7, body) {
|
|
2259
|
+
return new Promise((resolve6, reject) => {
|
|
2260
|
+
const req = http.request({
|
|
2261
|
+
hostname: "127.0.0.1",
|
|
2262
|
+
port,
|
|
2263
|
+
path: path7,
|
|
2264
|
+
method: "POST",
|
|
2265
|
+
headers: {
|
|
2266
|
+
"Content-Type": "application/json",
|
|
2267
|
+
"Content-Length": Buffer.byteLength(body)
|
|
2268
|
+
},
|
|
2269
|
+
timeout: 12e4
|
|
2270
|
+
// 2 minutes — context queries can be slow on first load
|
|
2271
|
+
}, (res) => {
|
|
2272
|
+
const chunks = [];
|
|
2273
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
2274
|
+
res.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
2275
|
+
});
|
|
2276
|
+
req.on("error", reject);
|
|
2277
|
+
req.on("timeout", () => {
|
|
2278
|
+
req.destroy();
|
|
2279
|
+
reject(new Error("Request timed out"));
|
|
2280
|
+
});
|
|
2281
|
+
req.write(body);
|
|
2282
|
+
req.end();
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
__name(httpPost, "httpPost");
|
|
2286
|
+
function httpGet(port, path7) {
|
|
2287
|
+
return new Promise((resolve6, reject) => {
|
|
2288
|
+
const req = http.request({
|
|
2289
|
+
hostname: "127.0.0.1",
|
|
2290
|
+
port,
|
|
2291
|
+
path: path7,
|
|
2292
|
+
method: "GET",
|
|
2293
|
+
timeout: 5e3
|
|
2294
|
+
}, (res) => {
|
|
2295
|
+
const chunks = [];
|
|
2296
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
2297
|
+
res.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
2298
|
+
});
|
|
2299
|
+
req.on("error", reject);
|
|
2300
|
+
req.on("timeout", () => {
|
|
2301
|
+
req.destroy();
|
|
2302
|
+
reject(new Error("Request timed out"));
|
|
2303
|
+
});
|
|
2304
|
+
req.end();
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
__name(httpGet, "httpGet");
|
|
2308
|
+
|
|
2309
|
+
// src/cli/commands/search.ts
|
|
2310
|
+
function parseSourceFlags() {
|
|
2311
|
+
const NON_SOURCE_FLAGS = /* @__PURE__ */ new Set([
|
|
2312
|
+
"repo",
|
|
2313
|
+
"depth",
|
|
2314
|
+
"collection",
|
|
2315
|
+
"pattern",
|
|
2316
|
+
"context",
|
|
2317
|
+
"name",
|
|
2318
|
+
"keep",
|
|
2319
|
+
"pruner",
|
|
2320
|
+
"only",
|
|
2321
|
+
"docs-path",
|
|
2322
|
+
"mode",
|
|
2323
|
+
"limit",
|
|
2324
|
+
"ignore",
|
|
2325
|
+
"include",
|
|
2326
|
+
"meta",
|
|
2327
|
+
"k",
|
|
2328
|
+
"yes",
|
|
2329
|
+
"y",
|
|
2330
|
+
"force",
|
|
2331
|
+
"verbose",
|
|
2332
|
+
"path"
|
|
2333
|
+
]);
|
|
2334
|
+
const sources = {};
|
|
2335
|
+
const positional = [];
|
|
2336
|
+
for (let i = 0; i < args.length; i++) {
|
|
2337
|
+
if (args[i].startsWith("--")) {
|
|
2338
|
+
const name = args[i].slice(2);
|
|
2339
|
+
if (name === "yes" || name === "force" || name === "verbose") continue;
|
|
2340
|
+
const next = args[i + 1];
|
|
2341
|
+
if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE_FLAGS.has(name)) {
|
|
2342
|
+
sources[name] = parseInt(next, 10);
|
|
2343
|
+
i++;
|
|
2344
|
+
continue;
|
|
473
2345
|
}
|
|
2346
|
+
if (NON_SOURCE_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
|
|
2347
|
+
i++;
|
|
2348
|
+
}
|
|
2349
|
+
continue;
|
|
474
2350
|
}
|
|
475
|
-
|
|
476
|
-
return;
|
|
2351
|
+
positional.push(args[i]);
|
|
477
2352
|
}
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
2353
|
+
const query = positional.slice(1).join(" ");
|
|
2354
|
+
return { sources, query };
|
|
2355
|
+
}
|
|
2356
|
+
__name(parseSourceFlags, "parseSourceFlags");
|
|
2357
|
+
function parsePaths() {
|
|
2358
|
+
const raw = getFlag("path");
|
|
2359
|
+
if (!raw) return void 0;
|
|
2360
|
+
const paths = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2361
|
+
return paths.length === 1 ? paths[0] : paths;
|
|
2362
|
+
}
|
|
2363
|
+
__name(parsePaths, "parsePaths");
|
|
2364
|
+
function printFilterInfo(sources, pathPrefix) {
|
|
2365
|
+
const parts = [];
|
|
2366
|
+
const entries = Object.entries(sources);
|
|
2367
|
+
if (entries.length > 0) parts.push(...entries.map(([k, v]) => `${k}=${v}`));
|
|
2368
|
+
if (pathPrefix) {
|
|
2369
|
+
const paths = Array.isArray(pathPrefix) ? pathPrefix : [pathPrefix];
|
|
2370
|
+
parts.push(`path=${paths.join(",")}`);
|
|
484
2371
|
}
|
|
485
|
-
|
|
486
|
-
const context = await brain.getContext(task);
|
|
487
|
-
console.log(context);
|
|
488
|
-
brain.close();
|
|
2372
|
+
if (parts.length > 0) console.log(c.dim(` Filters: ${parts.join(", ")}`));
|
|
489
2373
|
}
|
|
490
|
-
__name(
|
|
2374
|
+
__name(printFilterInfo, "printFilterInfo");
|
|
2375
|
+
function buildSearchOptions(sources, pathPrefix) {
|
|
2376
|
+
const opts = {
|
|
2377
|
+
sources: Object.keys(sources).length > 0 ? sources : {},
|
|
2378
|
+
source: "cli"
|
|
2379
|
+
};
|
|
2380
|
+
if (pathPrefix) opts.pathPrefix = pathPrefix;
|
|
2381
|
+
return opts;
|
|
2382
|
+
}
|
|
2383
|
+
__name(buildSearchOptions, "buildSearchOptions");
|
|
491
2384
|
async function cmdSearch() {
|
|
492
|
-
const query =
|
|
2385
|
+
const { sources, query } = parseSourceFlags();
|
|
493
2386
|
if (!query) {
|
|
494
|
-
console.log(c.red("Usage: brainbank search <query>"));
|
|
2387
|
+
console.log(c.red("Usage: brainbank search <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]"));
|
|
495
2388
|
process.exit(1);
|
|
496
2389
|
}
|
|
2390
|
+
const pathPrefix = parsePaths();
|
|
2391
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2392
|
+
const delegated = await tryServerSearch("search", {
|
|
2393
|
+
query,
|
|
2394
|
+
repo,
|
|
2395
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2396
|
+
pathPrefix
|
|
2397
|
+
});
|
|
2398
|
+
if (delegated) {
|
|
2399
|
+
console.log(c.bold(`
|
|
2400
|
+
\u2501\u2501\u2501 BrainBank Search: "${query}" \u2501\u2501\u2501
|
|
2401
|
+
`));
|
|
2402
|
+
printFilterInfo(sources, pathPrefix);
|
|
2403
|
+
printResults(delegated);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
497
2406
|
const brain = await createBrain();
|
|
498
2407
|
console.log(c.bold(`
|
|
499
2408
|
\u2501\u2501\u2501 BrainBank Search: "${query}" \u2501\u2501\u2501
|
|
500
2409
|
`));
|
|
501
|
-
|
|
2410
|
+
printFilterInfo(sources, pathPrefix);
|
|
2411
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2412
|
+
const results = await brain.search(query, opts);
|
|
502
2413
|
printResults(results);
|
|
503
2414
|
brain.close();
|
|
504
2415
|
}
|
|
505
2416
|
__name(cmdSearch, "cmdSearch");
|
|
506
2417
|
async function cmdHybridSearch() {
|
|
507
|
-
const query =
|
|
2418
|
+
const { sources, query } = parseSourceFlags();
|
|
508
2419
|
if (!query) {
|
|
509
|
-
console.log(c.red("Usage: brainbank hsearch <query>"));
|
|
2420
|
+
console.log(c.red("Usage: brainbank hsearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>] [--docs <n>]"));
|
|
510
2421
|
process.exit(1);
|
|
511
2422
|
}
|
|
2423
|
+
const pathPrefix = parsePaths();
|
|
2424
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2425
|
+
const delegated = await tryServerSearch("hybrid", {
|
|
2426
|
+
query,
|
|
2427
|
+
repo,
|
|
2428
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2429
|
+
pathPrefix
|
|
2430
|
+
});
|
|
2431
|
+
if (delegated) {
|
|
2432
|
+
console.log(c.bold(`
|
|
2433
|
+
\u2501\u2501\u2501 BrainBank Hybrid Search: "${query}" \u2501\u2501\u2501`));
|
|
2434
|
+
console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion`));
|
|
2435
|
+
printFilterInfo(sources, pathPrefix);
|
|
2436
|
+
console.log("");
|
|
2437
|
+
printResults(delegated);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
512
2440
|
const brain = await createBrain();
|
|
513
2441
|
console.log(c.bold(`
|
|
514
2442
|
\u2501\u2501\u2501 BrainBank Hybrid Search: "${query}" \u2501\u2501\u2501`));
|
|
515
|
-
console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion
|
|
516
|
-
|
|
517
|
-
|
|
2443
|
+
console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion`));
|
|
2444
|
+
printFilterInfo(sources, pathPrefix);
|
|
2445
|
+
console.log("");
|
|
2446
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2447
|
+
const results = await brain.hybridSearch(query, opts);
|
|
518
2448
|
printResults(results);
|
|
519
2449
|
brain.close();
|
|
520
2450
|
}
|
|
521
2451
|
__name(cmdHybridSearch, "cmdHybridSearch");
|
|
522
2452
|
async function cmdKeywordSearch() {
|
|
523
|
-
const query =
|
|
2453
|
+
const { sources, query } = parseSourceFlags();
|
|
524
2454
|
if (!query) {
|
|
525
|
-
console.log(c.red("Usage: brainbank ksearch <query>"));
|
|
2455
|
+
console.log(c.red("Usage: brainbank ksearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]"));
|
|
526
2456
|
process.exit(1);
|
|
527
2457
|
}
|
|
2458
|
+
const pathPrefix = parsePaths();
|
|
2459
|
+
const repo = getFlag("repo") ?? process.cwd();
|
|
2460
|
+
const delegated = await tryServerSearch("keyword", {
|
|
2461
|
+
query,
|
|
2462
|
+
repo,
|
|
2463
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2464
|
+
pathPrefix
|
|
2465
|
+
});
|
|
2466
|
+
if (delegated) {
|
|
2467
|
+
console.log(c.bold(`
|
|
2468
|
+
\u2501\u2501\u2501 BrainBank Keyword Search: "${query}" \u2501\u2501\u2501`));
|
|
2469
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
2470
|
+
printFilterInfo(sources, pathPrefix);
|
|
2471
|
+
console.log("");
|
|
2472
|
+
printResults(delegated, 0.4);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
528
2475
|
const brain = await createBrain();
|
|
529
2476
|
await brain.initialize();
|
|
530
2477
|
console.log(c.bold(`
|
|
531
2478
|
\u2501\u2501\u2501 BrainBank Keyword Search: "${query}" \u2501\u2501\u2501`));
|
|
532
|
-
console.log(c.dim(` Mode: BM25 full-text (instant)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
2479
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
2480
|
+
printFilterInfo(sources, pathPrefix);
|
|
2481
|
+
console.log("");
|
|
2482
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
2483
|
+
const results = await brain.searchBM25(query, opts);
|
|
2484
|
+
printResults(results, 0.4);
|
|
536
2485
|
brain.close();
|
|
537
2486
|
}
|
|
538
2487
|
__name(cmdKeywordSearch, "cmdKeywordSearch");
|
|
539
|
-
|
|
2488
|
+
|
|
2489
|
+
// src/cli/commands/query.ts
|
|
2490
|
+
import * as fs5 from "fs";
|
|
2491
|
+
function parseSourceFlags2() {
|
|
2492
|
+
const NON_SOURCE = /* @__PURE__ */ new Set([
|
|
2493
|
+
"repo",
|
|
2494
|
+
"depth",
|
|
2495
|
+
"collection",
|
|
2496
|
+
"pattern",
|
|
2497
|
+
"context",
|
|
2498
|
+
"pruner",
|
|
2499
|
+
"name",
|
|
2500
|
+
"keep",
|
|
2501
|
+
"only",
|
|
2502
|
+
"docs-path",
|
|
2503
|
+
"mode",
|
|
2504
|
+
"limit",
|
|
2505
|
+
"ignore",
|
|
2506
|
+
"meta",
|
|
2507
|
+
"k",
|
|
2508
|
+
"yes",
|
|
2509
|
+
"y",
|
|
2510
|
+
"force",
|
|
2511
|
+
"verbose",
|
|
2512
|
+
"path"
|
|
2513
|
+
]);
|
|
2514
|
+
const sources = {};
|
|
2515
|
+
for (let i = 0; i < args.length; i++) {
|
|
2516
|
+
if (!args[i].startsWith("--")) continue;
|
|
2517
|
+
const name = args[i].slice(2);
|
|
2518
|
+
if (name.startsWith("no-")) {
|
|
2519
|
+
sources[name.slice(3)] = 0;
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
const next = args[i + 1];
|
|
2523
|
+
if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE.has(name)) {
|
|
2524
|
+
sources[name] = parseInt(next, 10);
|
|
2525
|
+
i++;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return sources;
|
|
2529
|
+
}
|
|
2530
|
+
__name(parseSourceFlags2, "parseSourceFlags");
|
|
2531
|
+
function readFlagValue(flagName) {
|
|
2532
|
+
const raw = getFlag(flagName);
|
|
2533
|
+
if (!raw) return void 0;
|
|
2534
|
+
if (raw.startsWith("@")) {
|
|
2535
|
+
const filePath = raw.slice(1);
|
|
2536
|
+
try {
|
|
2537
|
+
return fs5.readFileSync(filePath, "utf-8").trim();
|
|
2538
|
+
} catch {
|
|
2539
|
+
console.error(c.red(`Cannot read ${flagName} file: ${filePath}`));
|
|
2540
|
+
process.exit(1);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return raw;
|
|
2544
|
+
}
|
|
2545
|
+
__name(readFlagValue, "readFlagValue");
|
|
2546
|
+
async function cmdQuery() {
|
|
2547
|
+
const task = stripFlags(args).slice(1).join(" ");
|
|
2548
|
+
if (!task) {
|
|
2549
|
+
console.log(c.red("Usage: brainbank query <task description>"));
|
|
2550
|
+
console.log(c.dim(" Options:"));
|
|
2551
|
+
console.log(c.dim(" --context <desc|@file> General task context for the pruner"));
|
|
2552
|
+
console.log(c.dim(" --pruner <desc> Specific pruning focus"));
|
|
2553
|
+
console.log(c.dim(" --path <dir> Filter to files under path"));
|
|
2554
|
+
console.log(c.dim(" --code N --git N Source limits"));
|
|
2555
|
+
process.exit(1);
|
|
2556
|
+
}
|
|
2557
|
+
const sources = parseSourceFlags2();
|
|
2558
|
+
const rawPath = getFlag("path");
|
|
2559
|
+
const pathPrefix = rawPath ? rawPath.split(",").map((p) => p.trim()).filter(Boolean) : void 0;
|
|
2560
|
+
const normalizedPath = pathPrefix && pathPrefix.length === 1 ? pathPrefix[0] : pathPrefix;
|
|
2561
|
+
const ignorePaths = getFlagAll("ignore");
|
|
2562
|
+
const repo = getFlag("repo");
|
|
2563
|
+
const contextDesc = readFlagValue("context");
|
|
2564
|
+
const prunerDesc = readFlagValue("pruner");
|
|
2565
|
+
const fields = parseFieldFlags();
|
|
2566
|
+
const serverResult = await tryServerContext({
|
|
2567
|
+
task,
|
|
2568
|
+
repo: repo ?? process.cwd(),
|
|
2569
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2570
|
+
pathPrefix: normalizedPath,
|
|
2571
|
+
ignorePaths: ignorePaths.length > 0 ? ignorePaths : void 0,
|
|
2572
|
+
fields: Object.keys(fields).length > 0 ? fields : void 0,
|
|
2573
|
+
context: contextDesc,
|
|
2574
|
+
prunerContext: prunerDesc
|
|
2575
|
+
});
|
|
2576
|
+
if (serverResult !== null) {
|
|
2577
|
+
console.log(serverResult);
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const brain = await createBrain();
|
|
2581
|
+
const result = await brain.getContext(task, {
|
|
2582
|
+
sources: Object.keys(sources).length > 0 ? sources : void 0,
|
|
2583
|
+
pathPrefix: normalizedPath,
|
|
2584
|
+
ignorePaths: ignorePaths.length > 0 ? ignorePaths : void 0,
|
|
2585
|
+
source: "cli",
|
|
2586
|
+
fields: Object.keys(fields).length > 0 ? fields : void 0,
|
|
2587
|
+
context: contextDesc,
|
|
2588
|
+
prunerContext: prunerDesc
|
|
2589
|
+
});
|
|
2590
|
+
console.log(result);
|
|
2591
|
+
brain.close();
|
|
2592
|
+
}
|
|
2593
|
+
__name(cmdQuery, "cmdQuery");
|
|
2594
|
+
function parseFieldFlags() {
|
|
2595
|
+
const FIELD_BOOLEANS = /* @__PURE__ */ new Set(["lines", "symbols", "compact"]);
|
|
2596
|
+
const FIELD_NEGATABLE = /* @__PURE__ */ new Set(["callTree", "imports"]);
|
|
2597
|
+
const fields = {};
|
|
2598
|
+
for (let i = 0; i < args.length; i++) {
|
|
2599
|
+
if (!args[i].startsWith("--")) continue;
|
|
2600
|
+
const raw = args[i].slice(2);
|
|
2601
|
+
if (raw.startsWith("no-")) {
|
|
2602
|
+
const name = raw.slice(3);
|
|
2603
|
+
if (FIELD_NEGATABLE.has(name)) {
|
|
2604
|
+
fields[name] = false;
|
|
2605
|
+
}
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
const dotIdx = raw.indexOf(".");
|
|
2609
|
+
if (dotIdx > 0) {
|
|
2610
|
+
const fieldName = raw.slice(0, dotIdx);
|
|
2611
|
+
const rest = raw.slice(dotIdx + 1);
|
|
2612
|
+
const eqIdx = rest.indexOf("=");
|
|
2613
|
+
if (eqIdx > 0) {
|
|
2614
|
+
const key = rest.slice(0, eqIdx);
|
|
2615
|
+
const val = parseInt(rest.slice(eqIdx + 1), 10);
|
|
2616
|
+
if (!isNaN(val)) {
|
|
2617
|
+
fields[fieldName] = { [key]: val };
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (FIELD_BOOLEANS.has(raw)) {
|
|
2623
|
+
fields[raw] = true;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
return fields;
|
|
2627
|
+
}
|
|
2628
|
+
__name(parseFieldFlags, "parseFieldFlags");
|
|
2629
|
+
|
|
2630
|
+
// src/cli/commands/context.ts
|
|
2631
|
+
async function cmdContext() {
|
|
2632
|
+
const pos = stripFlags(args);
|
|
2633
|
+
const sub = pos[1];
|
|
2634
|
+
if (sub === "add") {
|
|
2635
|
+
const collection = pos[2];
|
|
2636
|
+
const path7 = pos[3];
|
|
2637
|
+
const desc = pos.slice(4).join(" ");
|
|
2638
|
+
if (!collection || !path7 || !desc) {
|
|
2639
|
+
console.log(c.red("Usage: brainbank context add <collection> <path> <description>"));
|
|
2640
|
+
process.exit(1);
|
|
2641
|
+
}
|
|
2642
|
+
const brain = await createBrain();
|
|
2643
|
+
await brain.initialize();
|
|
2644
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2645
|
+
if (!docsPlugin) {
|
|
2646
|
+
console.log(c.red("Docs plugin not loaded."));
|
|
2647
|
+
process.exit(1);
|
|
2648
|
+
}
|
|
2649
|
+
docsPlugin.addContext(collection, path7, desc);
|
|
2650
|
+
console.log(c.green(`\u2713 Context added: ${collection}:${path7} \u2192 "${desc}"`));
|
|
2651
|
+
brain.close();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (sub === "list") {
|
|
2655
|
+
const brain = await createBrain();
|
|
2656
|
+
await brain.initialize();
|
|
2657
|
+
const docsPlugin = findDocsPlugin(brain);
|
|
2658
|
+
if (!docsPlugin) {
|
|
2659
|
+
console.log(c.yellow(" Docs plugin not loaded."));
|
|
2660
|
+
brain.close();
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
const contexts = docsPlugin.listContexts();
|
|
2664
|
+
if (contexts.length === 0) {
|
|
2665
|
+
console.log(c.yellow(" No contexts configured."));
|
|
2666
|
+
} else {
|
|
2667
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Contexts \u2501\u2501\u2501\n"));
|
|
2668
|
+
for (const ctx of contexts) {
|
|
2669
|
+
console.log(` ${c.cyan(ctx.collection)}:${ctx.path} \u2192 ${c.dim(ctx.context)}`);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
brain.close();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
console.error(c.yellow("\u26A0 `brainbank context` is deprecated \u2014 use `brainbank query` instead.\n"));
|
|
2676
|
+
return cmdQuery();
|
|
2677
|
+
}
|
|
2678
|
+
__name(cmdContext, "cmdContext");
|
|
2679
|
+
|
|
2680
|
+
// src/cli/commands/files.ts
|
|
2681
|
+
async function cmdFiles() {
|
|
2682
|
+
const patterns = [];
|
|
2683
|
+
const showLines = args.includes("--lines");
|
|
2684
|
+
for (let i = 1; i < args.length; i++) {
|
|
2685
|
+
if (args[i].startsWith("--")) {
|
|
2686
|
+
if (args[i] === "--repo") {
|
|
2687
|
+
i++;
|
|
2688
|
+
}
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
patterns.push(args[i]);
|
|
2692
|
+
}
|
|
2693
|
+
if (patterns.length === 0) {
|
|
2694
|
+
console.log(c.red("Usage: brainbank files <path|glob> [...paths] [--lines]"));
|
|
2695
|
+
console.log(c.dim(" Exact: brainbank files src/auth/login.ts"));
|
|
2696
|
+
console.log(c.dim(" Directory: brainbank files src/graph/"));
|
|
2697
|
+
console.log(c.dim(' Glob: brainbank files "src/**/*.service.ts"'));
|
|
2698
|
+
console.log(c.dim(" Fuzzy: brainbank files plugin.ts"));
|
|
2699
|
+
console.log(c.dim(" Lines: brainbank files src/plugin.ts --lines"));
|
|
2700
|
+
process.exit(1);
|
|
2701
|
+
}
|
|
2702
|
+
const brain = await createBrain();
|
|
2703
|
+
await brain.initialize();
|
|
2704
|
+
const results = brain.resolveFiles(patterns);
|
|
540
2705
|
if (results.length === 0) {
|
|
541
|
-
console.log(c.yellow("
|
|
2706
|
+
console.log(c.yellow("No matching files found in the index."));
|
|
2707
|
+
console.log(c.dim("Run `brainbank index` first to index your codebase."));
|
|
2708
|
+
brain.close();
|
|
542
2709
|
return;
|
|
543
2710
|
}
|
|
544
2711
|
for (const r of results) {
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
} else
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
2712
|
+
const meta = r.metadata;
|
|
2713
|
+
const startLine = meta.startLine ?? 1;
|
|
2714
|
+
console.log(c.bold(`
|
|
2715
|
+
\u2500\u2500 ${r.filePath} \u2500\u2500
|
|
2716
|
+
`));
|
|
2717
|
+
if (showLines) {
|
|
2718
|
+
const codeLines = r.content.split("\n");
|
|
2719
|
+
const pad = String(startLine + codeLines.length - 1).length;
|
|
2720
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
2721
|
+
const lineNum = c.dim(`${String(startLine + i).padStart(pad)}|`);
|
|
2722
|
+
console.log(`${lineNum} ${codeLines[i]}`);
|
|
2723
|
+
}
|
|
2724
|
+
} else {
|
|
2725
|
+
console.log(r.content);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
console.log(c.dim(`
|
|
2729
|
+
${results.length} file(s) resolved.`));
|
|
2730
|
+
brain.close();
|
|
2731
|
+
}
|
|
2732
|
+
__name(cmdFiles, "cmdFiles");
|
|
2733
|
+
|
|
2734
|
+
// src/cli/commands/stats.ts
|
|
2735
|
+
import * as path5 from "path";
|
|
2736
|
+
function formatStatKey(key) {
|
|
2737
|
+
return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase()).padEnd(16);
|
|
2738
|
+
}
|
|
2739
|
+
__name(formatStatKey, "formatStatKey");
|
|
567
2740
|
async function cmdStats() {
|
|
2741
|
+
const plain = args.includes("--plain");
|
|
2742
|
+
if (!plain) {
|
|
2743
|
+
try {
|
|
2744
|
+
const repoPath = path5.resolve(getFlag("repo") ?? process.cwd());
|
|
2745
|
+
const dbPath = path5.join(repoPath, ".brainbank", "data", "brainbank.db");
|
|
2746
|
+
const configPath = path5.join(repoPath, ".brainbank", "config.json");
|
|
2747
|
+
const { runStatsTui } = await import("./stats-tui-AD3AMYGV.js");
|
|
2748
|
+
await runStatsTui(dbPath, repoPath, configPath);
|
|
2749
|
+
return;
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
if (err instanceof Error && err.message.includes("ENOENT")) {
|
|
2752
|
+
console.log(c.yellow("No BrainBank database found. Run `brainbank index` first.\n"));
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
568
2757
|
const brain = await createBrain();
|
|
569
2758
|
await brain.initialize();
|
|
570
2759
|
const s = brain.stats();
|
|
571
2760
|
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Stats \u2501\u2501\u2501\n"));
|
|
572
|
-
console.log(` ${c.cyan("
|
|
2761
|
+
console.log(` ${c.cyan("Plugins")}: ${brain.plugins.join(", ")}
|
|
573
2762
|
`);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
console.log(`
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
if (s.git) {
|
|
582
|
-
console.log(` ${c.cyan("Git History")}`);
|
|
583
|
-
console.log(` Commits: ${s.git.commits}`);
|
|
584
|
-
console.log(` Files tracked: ${s.git.filesTracked}`);
|
|
585
|
-
console.log(` Co-edit pairs: ${s.git.coEdits}`);
|
|
586
|
-
console.log(` HNSW vectors: ${s.git.hnswSize}`);
|
|
587
|
-
console.log("");
|
|
588
|
-
}
|
|
589
|
-
if (s.documents) {
|
|
590
|
-
console.log(` ${c.cyan("Documents")}`);
|
|
591
|
-
console.log(` Collections: ${s.documents.collections}`);
|
|
592
|
-
console.log(` Documents: ${s.documents.documents}`);
|
|
593
|
-
console.log(` Chunks: ${s.documents.chunks}`);
|
|
594
|
-
console.log(` HNSW vectors: ${s.documents.hnswSize}`);
|
|
2763
|
+
for (const [name, pluginStats] of Object.entries(s)) {
|
|
2764
|
+
if (!pluginStats) continue;
|
|
2765
|
+
console.log(` ${c.cyan(name)}`);
|
|
2766
|
+
for (const [key, value] of Object.entries(pluginStats)) {
|
|
2767
|
+
console.log(` ${formatStatKey(key)}${value}`);
|
|
2768
|
+
}
|
|
595
2769
|
console.log("");
|
|
596
2770
|
}
|
|
597
2771
|
const kvNames = brain.listCollectionNames();
|
|
@@ -606,6 +2780,8 @@ async function cmdStats() {
|
|
|
606
2780
|
brain.close();
|
|
607
2781
|
}
|
|
608
2782
|
__name(cmdStats, "cmdStats");
|
|
2783
|
+
|
|
2784
|
+
// src/cli/commands/reembed.ts
|
|
609
2785
|
async function cmdReembed() {
|
|
610
2786
|
const brain = await createBrain();
|
|
611
2787
|
await brain.initialize();
|
|
@@ -618,29 +2794,69 @@ async function cmdReembed() {
|
|
|
618
2794
|
}, "onProgress")
|
|
619
2795
|
});
|
|
620
2796
|
console.log("\n");
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
2797
|
+
for (const [name, count] of Object.entries(result.counts)) {
|
|
2798
|
+
if (count > 0) {
|
|
2799
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2800
|
+
console.log(` ${c.green("\u2713")} ${label.padEnd(8)} ${count} vectors`);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
627
2803
|
console.log(`
|
|
628
2804
|
${c.bold("Total")}: ${result.total} vectors regenerated
|
|
629
2805
|
`);
|
|
630
2806
|
brain.close();
|
|
631
2807
|
}
|
|
632
2808
|
__name(cmdReembed, "cmdReembed");
|
|
2809
|
+
|
|
2810
|
+
// src/cli/commands/reindex.ts
|
|
2811
|
+
import * as fs6 from "fs";
|
|
2812
|
+
import * as path6 from "path";
|
|
2813
|
+
async function cmdReindex() {
|
|
2814
|
+
const positional = stripFlags(args);
|
|
2815
|
+
const repoPath = path6.resolve(positional[1] || ".");
|
|
2816
|
+
const dataDir = path6.join(repoPath, ".brainbank", "data");
|
|
2817
|
+
if (fs6.existsSync(dataDir)) {
|
|
2818
|
+
const entries = fs6.readdirSync(dataDir);
|
|
2819
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Reindex \u2501\u2501\u2501\n"));
|
|
2820
|
+
console.log(` ${c.yellow("\u2717")} Deleting ${path6.relative(process.cwd(), dataDir)}/ (${entries.length} files)`);
|
|
2821
|
+
fs6.rmSync(dataDir, { recursive: true, force: true });
|
|
2822
|
+
console.log(` ${c.green("\u2713")} Data cleared
|
|
2823
|
+
`);
|
|
2824
|
+
} else {
|
|
2825
|
+
console.log(c.bold("\n\u2501\u2501\u2501 Reindex \u2501\u2501\u2501\n"));
|
|
2826
|
+
console.log(c.dim(" No existing data \u2014 fresh index\n"));
|
|
2827
|
+
}
|
|
2828
|
+
if (!args.includes("--yes") && !args.includes("-y")) {
|
|
2829
|
+
args.push("--yes");
|
|
2830
|
+
}
|
|
2831
|
+
if (!args.includes("--force")) {
|
|
2832
|
+
args.push("--force");
|
|
2833
|
+
}
|
|
2834
|
+
return cmdIndex();
|
|
2835
|
+
}
|
|
2836
|
+
__name(cmdReindex, "cmdReindex");
|
|
2837
|
+
|
|
2838
|
+
// src/cli/commands/watch.ts
|
|
633
2839
|
async function cmdWatch() {
|
|
634
2840
|
const brain = await createBrain();
|
|
635
2841
|
await brain.initialize();
|
|
2842
|
+
const config = await loadConfig(brain.config.repoPath);
|
|
2843
|
+
const codeIgnore = config?.code?.ignore ?? [];
|
|
2844
|
+
const codeInclude = config?.code?.include ?? [];
|
|
636
2845
|
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Watch \u2501\u2501\u2501\n"));
|
|
637
2846
|
console.log(c.dim(` Watching ${brain.config.repoPath} for changes...`));
|
|
2847
|
+
if (codeInclude.length > 0) {
|
|
2848
|
+
console.log(c.dim(` Include: ${codeInclude.join(", ")}`));
|
|
2849
|
+
}
|
|
2850
|
+
if (codeIgnore.length > 0) {
|
|
2851
|
+
console.log(c.dim(` Ignoring: ${codeIgnore.join(", ")}`));
|
|
2852
|
+
}
|
|
638
2853
|
console.log(c.dim(" Press Ctrl+C to stop.\n"));
|
|
639
2854
|
const watcher = brain.watch({
|
|
640
2855
|
debounceMs: 2e3,
|
|
641
|
-
|
|
2856
|
+
ignore: codeIgnore,
|
|
2857
|
+
onIndex: /* @__PURE__ */ __name((sourceId, pluginName) => {
|
|
642
2858
|
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
643
|
-
console.log(` ${c.dim(ts)} ${c.green("\u2713")} ${c.cyan(
|
|
2859
|
+
console.log(` ${c.dim(ts)} ${c.green("\u2713")} ${c.cyan(pluginName)}: ${sourceId}`);
|
|
644
2860
|
}, "onIndex"),
|
|
645
2861
|
onError: /* @__PURE__ */ __name((err) => {
|
|
646
2862
|
console.error(` ${c.red("\u2717")} ${err.message}`);
|
|
@@ -656,14 +2872,135 @@ async function cmdWatch() {
|
|
|
656
2872
|
});
|
|
657
2873
|
}
|
|
658
2874
|
__name(cmdWatch, "cmdWatch");
|
|
659
|
-
|
|
660
|
-
|
|
2875
|
+
|
|
2876
|
+
// src/cli/commands/mcp.ts
|
|
2877
|
+
async function cmdMcp() {
|
|
2878
|
+
await import("./mcp.js");
|
|
2879
|
+
}
|
|
2880
|
+
__name(cmdMcp, "cmdMcp");
|
|
2881
|
+
|
|
2882
|
+
// src/cli/commands/daemon.ts
|
|
2883
|
+
async function cmdDaemon() {
|
|
2884
|
+
const pos = stripFlags(args);
|
|
2885
|
+
const sub = pos[1];
|
|
2886
|
+
if (sub === "stop") return stopDaemon();
|
|
2887
|
+
if (sub === "restart") {
|
|
2888
|
+
stopDaemon();
|
|
2889
|
+
return forkDaemon();
|
|
2890
|
+
}
|
|
2891
|
+
if (sub === "start") return forkDaemon();
|
|
2892
|
+
return startForeground();
|
|
2893
|
+
}
|
|
2894
|
+
__name(cmdDaemon, "cmdDaemon");
|
|
2895
|
+
async function startForeground() {
|
|
2896
|
+
const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
|
|
2897
|
+
const { HttpServer } = await import("./http-server-2ZQ6I43B.js");
|
|
2898
|
+
const server = new HttpServer({
|
|
2899
|
+
port,
|
|
2900
|
+
factory: /* @__PURE__ */ __name(async (repoPath) => {
|
|
2901
|
+
const brain = await createBrain(repoPath);
|
|
2902
|
+
await brain.initialize();
|
|
2903
|
+
return brain;
|
|
2904
|
+
}, "factory"),
|
|
2905
|
+
onError: /* @__PURE__ */ __name((repo, err) => {
|
|
2906
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2907
|
+
console.error(c.red(` Pool error [${repo}]: ${msg}`));
|
|
2908
|
+
}, "onError"),
|
|
2909
|
+
onLog: /* @__PURE__ */ __name((msg) => console.log(c.dim(` ${msg}`)), "onLog")
|
|
2910
|
+
});
|
|
2911
|
+
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank HTTP Daemon \u2501\u2501\u2501\n"));
|
|
2912
|
+
await server.start();
|
|
2913
|
+
console.log(c.dim(` Port: ${port}`));
|
|
2914
|
+
console.log(c.dim(" Press Ctrl+C to stop.\n"));
|
|
2915
|
+
const shutdown = /* @__PURE__ */ __name(() => {
|
|
2916
|
+
console.log(c.dim("\n Shutting down..."));
|
|
2917
|
+
server.close();
|
|
2918
|
+
process.exit(0);
|
|
2919
|
+
}, "shutdown");
|
|
2920
|
+
process.on("SIGINT", shutdown);
|
|
2921
|
+
process.on("SIGTERM", shutdown);
|
|
2922
|
+
await new Promise(() => {
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
__name(startForeground, "startForeground");
|
|
2926
|
+
async function forkDaemon() {
|
|
2927
|
+
const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
|
|
2928
|
+
const { fork } = await import("child_process");
|
|
2929
|
+
const existing = isServerRunning();
|
|
2930
|
+
if (existing) {
|
|
2931
|
+
console.log(c.yellow(` Daemon already running (PID ${existing.pid}, port ${existing.port})`));
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
const child = fork(process.argv[1], ["daemon", "--port", String(port)], {
|
|
2935
|
+
detached: true,
|
|
2936
|
+
stdio: "ignore"
|
|
2937
|
+
});
|
|
2938
|
+
child.unref();
|
|
2939
|
+
console.log(c.green(` \u2713 Daemon started (PID ${child.pid}, port ${port})`));
|
|
2940
|
+
console.log(c.dim(" Stop with: brainbank daemon stop"));
|
|
2941
|
+
}
|
|
2942
|
+
__name(forkDaemon, "forkDaemon");
|
|
2943
|
+
function stopDaemon() {
|
|
2944
|
+
const info = isServerRunning();
|
|
2945
|
+
if (!info) {
|
|
2946
|
+
console.log(c.yellow(" No daemon running."));
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
try {
|
|
2950
|
+
process.kill(info.pid, "SIGTERM");
|
|
2951
|
+
removePid();
|
|
2952
|
+
console.log(c.green(` \u2713 Daemon stopped (PID ${info.pid})`));
|
|
2953
|
+
} catch {
|
|
2954
|
+
removePid();
|
|
2955
|
+
console.log(c.yellow(` PID ${info.pid} not found. Cleaned up stale PID file.`));
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
__name(stopDaemon, "stopDaemon");
|
|
2959
|
+
|
|
2960
|
+
// src/cli/commands/status.ts
|
|
2961
|
+
function formatUptime(seconds) {
|
|
2962
|
+
if (seconds < 60) return `${seconds}s`;
|
|
2963
|
+
const minutes = Math.floor(seconds / 60);
|
|
2964
|
+
if (minutes < 60) return `${minutes}m`;
|
|
2965
|
+
const hours = Math.floor(minutes / 60);
|
|
2966
|
+
const remainMinutes = minutes % 60;
|
|
2967
|
+
return remainMinutes > 0 ? `${hours}h ${remainMinutes}m` : `${hours}h`;
|
|
2968
|
+
}
|
|
2969
|
+
__name(formatUptime, "formatUptime");
|
|
2970
|
+
async function cmdStatus() {
|
|
2971
|
+
const info = isServerRunning();
|
|
2972
|
+
if (!info) {
|
|
2973
|
+
console.log(`
|
|
2974
|
+
${c.dim("HTTP Server:")} ${c.yellow("stopped")}
|
|
2975
|
+
`);
|
|
2976
|
+
console.log(c.dim(" Start with: brainbank daemon"));
|
|
2977
|
+
console.log("");
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
const health = await serverHealth();
|
|
2981
|
+
if (health) {
|
|
2982
|
+
const uptime = formatUptime(health.uptime);
|
|
2983
|
+
console.log(`
|
|
2984
|
+
${c.dim("HTTP Server:")} ${c.green("running")}`);
|
|
2985
|
+
console.log(` ${c.dim("PID:")} ${health.pid}`);
|
|
2986
|
+
console.log(` ${c.dim("Port:")} ${health.port}`);
|
|
2987
|
+
console.log(` ${c.dim("Uptime:")} ${uptime}`);
|
|
2988
|
+
console.log(` ${c.dim("Workspaces:")} ${health.workspaces}`);
|
|
2989
|
+
console.log("");
|
|
2990
|
+
} else {
|
|
2991
|
+
console.log(`
|
|
2992
|
+
${c.dim("HTTP Server:")} ${c.yellow("stale")} (PID ${info.pid} not responding)`);
|
|
2993
|
+
console.log(c.dim(" The PID file may be stale. Restart with: brainbank daemon"));
|
|
2994
|
+
console.log("");
|
|
2995
|
+
}
|
|
661
2996
|
}
|
|
662
|
-
__name(
|
|
2997
|
+
__name(cmdStatus, "cmdStatus");
|
|
2998
|
+
|
|
2999
|
+
// src/cli/commands/help.ts
|
|
663
3000
|
function showHelp() {
|
|
664
3001
|
console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2014 Semantic Knowledge Bank \u2501\u2501\u2501\n"));
|
|
665
3002
|
console.log(c.bold("Indexing:"));
|
|
666
|
-
console.log(` ${c.cyan("index")} [path]
|
|
3003
|
+
console.log(` ${c.cyan("index")} ${c.dim("(i)")} [path] Index code + git history`);
|
|
667
3004
|
console.log(` ${c.cyan("collection add")} <path> --name Add a document collection`);
|
|
668
3005
|
console.log(` ${c.cyan("collection list")} List collections`);
|
|
669
3006
|
console.log(` ${c.cyan("collection remove")} <name> Remove a collection`);
|
|
@@ -674,11 +3011,14 @@ function showHelp() {
|
|
|
674
3011
|
console.log(` ${c.cyan("hsearch")} <query> Hybrid search (${c.bold("best quality")})`);
|
|
675
3012
|
console.log(` ${c.cyan("ksearch")} <query> Keyword search (BM25, instant)`);
|
|
676
3013
|
console.log(` ${c.cyan("dsearch")} <query> Document search`);
|
|
3014
|
+
console.log(c.dim(" All accept --repo <path> --path <dir> --<source> <n>"));
|
|
677
3015
|
console.log("");
|
|
678
|
-
console.log(c.bold("
|
|
679
|
-
console.log(` ${c.cyan("
|
|
3016
|
+
console.log(c.bold("Query:"));
|
|
3017
|
+
console.log(` ${c.cyan("query")} ${c.dim("(q)")} <task> Get AI-pruned context for a task`);
|
|
3018
|
+
console.log(` ${c.cyan("context")} <task> ${c.dim("(deprecated \u2192 use query)")}`);
|
|
680
3019
|
console.log(` ${c.cyan("context add")} <col> <path> <desc> Add context metadata`);
|
|
681
3020
|
console.log(` ${c.cyan("context list")} List all context metadata`);
|
|
3021
|
+
console.log(` ${c.cyan("files")} <path|glob> [...] [--lines] View full indexed files directly`);
|
|
682
3022
|
console.log("");
|
|
683
3023
|
console.log(c.bold("KV Store:"));
|
|
684
3024
|
console.log(` ${c.cyan("kv add")} <coll> <content> Add item to a collection`);
|
|
@@ -690,8 +3030,16 @@ function showHelp() {
|
|
|
690
3030
|
console.log(c.bold("Utility:"));
|
|
691
3031
|
console.log(` ${c.cyan("stats")} Show index statistics`);
|
|
692
3032
|
console.log(` ${c.cyan("reembed")} Re-embed all vectors`);
|
|
3033
|
+
console.log(` ${c.cyan("reindex")} [path] Nuke data + re-index from scratch`);
|
|
693
3034
|
console.log(` ${c.cyan("watch")} Watch files, auto-re-index`);
|
|
694
|
-
console.log(` ${c.cyan("
|
|
3035
|
+
console.log(` ${c.cyan("mcp")} Start MCP server (stdio)`);
|
|
3036
|
+
console.log(` ${c.cyan("mcp:export")} [target] Export MCP config (antigravity)`);
|
|
3037
|
+
console.log(` ${c.cyan("daemon")} Start HTTP daemon (foreground)`);
|
|
3038
|
+
console.log(` ${c.cyan("daemon start")} Start HTTP daemon (background)`);
|
|
3039
|
+
console.log(` ${c.cyan("daemon stop")} Stop background daemon`);
|
|
3040
|
+
console.log(` ${c.cyan("daemon restart")} Restart background daemon`);
|
|
3041
|
+
console.log(` ${c.cyan("status")} Show daemon status`);
|
|
3042
|
+
console.log(` ${c.cyan("--version")} ${c.dim("(-v)")} Show version`);
|
|
695
3043
|
console.log("");
|
|
696
3044
|
console.log(c.bold("Options:"));
|
|
697
3045
|
console.log(` ${c.dim("--repo <path>")} Repository path (default: .)`);
|
|
@@ -699,21 +3047,45 @@ function showHelp() {
|
|
|
699
3047
|
console.log(` ${c.dim("--depth <n>")} Git history depth (default: 500)`);
|
|
700
3048
|
console.log(` ${c.dim("--collection <name>")} Filter by collection`);
|
|
701
3049
|
console.log(` ${c.dim("--pattern <glob>")} Collection glob (default: **/*.md)`);
|
|
702
|
-
console.log(` ${c.dim("--context <desc>")}
|
|
703
|
-
console.log(` ${c.dim("--
|
|
3050
|
+
console.log(` ${c.dim("--context <desc>")} General task context for pruner (inline or @file)`);
|
|
3051
|
+
console.log(` ${c.dim("--pruner <desc>")} Specific pruning focus (inline or @file)`);
|
|
3052
|
+
console.log(` ${c.dim("--<source> <n>")} Source filter: max results from <source> (0 = skip)`);
|
|
3053
|
+
console.log(` ${c.dim("--path <dir>")} Filter context results to files under this path prefix`);
|
|
3054
|
+
console.log(` ${c.dim("--ignore <globs>")} Ignore glob patterns for code indexing (comma-separated)`);
|
|
3055
|
+
console.log(` ${c.dim("--include <globs>")} Include only these paths for code indexing (comma-separated)`);
|
|
3056
|
+
console.log(` ${c.dim("--yes / -y")} Skip interactive prompt (auto-select all available)`);
|
|
3057
|
+
console.log(` ${c.dim("--setup")} Re-run interactive setup (modules, folders, config)`);
|
|
3058
|
+
console.log(` ${c.dim("--port <n>")} HTTP daemon port (default: 8181)`);
|
|
704
3059
|
console.log("");
|
|
705
3060
|
console.log(c.bold("Examples:"));
|
|
706
3061
|
console.log(c.dim(" brainbank index ."));
|
|
3062
|
+
console.log(c.dim(' brainbank index . --ignore "sdk/**,vendor/**"'));
|
|
3063
|
+
console.log(c.dim(' brainbank index . --include "src/**,lib/**"'));
|
|
707
3064
|
console.log(c.dim(' brainbank kv add errors "Fixed null pointer in api.ts"'));
|
|
708
3065
|
console.log(c.dim(' brainbank kv search errors "null pointer"'));
|
|
709
3066
|
console.log(c.dim(" brainbank kv list"));
|
|
710
3067
|
console.log(c.dim(' brainbank hsearch "authentication middleware"'));
|
|
711
|
-
console.log(c.dim(' brainbank
|
|
712
|
-
console.log(c.dim(
|
|
3068
|
+
console.log(c.dim(' brainbank hsearch "auth" --code 0 --git 10 # git only'));
|
|
3069
|
+
console.log(c.dim(' brainbank search "handler" --git 0 # code only'));
|
|
3070
|
+
console.log(c.dim(' brainbank hsearch "api" --docs 10 --code 0 --git 0 # docs only'));
|
|
3071
|
+
console.log(c.dim(' brainbank query "auth flow" | pbcopy # \u2192 clipboard'));
|
|
3072
|
+
console.log(c.dim(' brainbank query "auth" --context @issue.md # with task context'));
|
|
3073
|
+
console.log(c.dim(' brainbank query "auth" --pruner "JWT refresh" # focused pruning'));
|
|
3074
|
+
console.log(c.dim(" brainbank daemon start # background HTTP"));
|
|
3075
|
+
console.log(c.dim(" brainbank mcp # MCP stdio"));
|
|
3076
|
+
console.log(c.dim(" brainbank mcp:export antigravity # export MCP config"));
|
|
713
3077
|
}
|
|
714
3078
|
__name(showHelp, "showHelp");
|
|
3079
|
+
|
|
3080
|
+
// src/cli/index.ts
|
|
3081
|
+
var command = args[0];
|
|
715
3082
|
async function main() {
|
|
716
3083
|
switch (command) {
|
|
3084
|
+
case "--version":
|
|
3085
|
+
case "-v":
|
|
3086
|
+
console.log(`brainbank v${VERSION}`);
|
|
3087
|
+
break;
|
|
3088
|
+
case "i":
|
|
717
3089
|
case "index":
|
|
718
3090
|
return cmdIndex();
|
|
719
3091
|
case "collection":
|
|
@@ -732,14 +3104,30 @@ async function main() {
|
|
|
732
3104
|
return cmdKeywordSearch();
|
|
733
3105
|
case "context":
|
|
734
3106
|
return cmdContext();
|
|
3107
|
+
case "q":
|
|
3108
|
+
case "query":
|
|
3109
|
+
return cmdQuery();
|
|
3110
|
+
case "files":
|
|
3111
|
+
return cmdFiles();
|
|
735
3112
|
case "stats":
|
|
736
3113
|
return cmdStats();
|
|
737
3114
|
case "reembed":
|
|
738
3115
|
return cmdReembed();
|
|
3116
|
+
case "reindex":
|
|
3117
|
+
return cmdReindex();
|
|
739
3118
|
case "watch":
|
|
740
3119
|
return cmdWatch();
|
|
3120
|
+
case "mcp":
|
|
3121
|
+
return cmdMcp();
|
|
3122
|
+
case "mcp:export":
|
|
3123
|
+
return cmdMcpExport();
|
|
741
3124
|
case "serve":
|
|
742
|
-
return
|
|
3125
|
+
return cmdMcp();
|
|
3126
|
+
// backward compat
|
|
3127
|
+
case "daemon":
|
|
3128
|
+
return cmdDaemon();
|
|
3129
|
+
case "status":
|
|
3130
|
+
return cmdStatus();
|
|
743
3131
|
case "help":
|
|
744
3132
|
case "--help":
|
|
745
3133
|
case "-h":
|