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.
Files changed (167) hide show
  1. package/README.md +84 -1107
  2. package/assets/architecture.png +0 -0
  3. package/bin/brainbank +8 -1
  4. package/bin/brainbank-mcp +19 -0
  5. package/dist/chunk-3UIWA32X.js +3341 -0
  6. package/dist/chunk-3UIWA32X.js.map +1 -0
  7. package/dist/chunk-3YBCD6DI.js +117 -0
  8. package/dist/chunk-3YBCD6DI.js.map +1 -0
  9. package/dist/chunk-DAGVUEXL.js +258 -0
  10. package/dist/chunk-DAGVUEXL.js.map +1 -0
  11. package/dist/chunk-DMFMTOHF.js +123 -0
  12. package/dist/chunk-DMFMTOHF.js.map +1 -0
  13. package/dist/chunk-FQYKWB2Q.js +136 -0
  14. package/dist/chunk-FQYKWB2Q.js.map +1 -0
  15. package/dist/chunk-IMJJ2VEM.js +74 -0
  16. package/dist/chunk-IMJJ2VEM.js.map +1 -0
  17. package/dist/chunk-M744PCJQ.js +43 -0
  18. package/dist/chunk-M744PCJQ.js.map +1 -0
  19. package/dist/chunk-NNDY7P2R.js +211 -0
  20. package/dist/chunk-NNDY7P2R.js.map +1 -0
  21. package/dist/chunk-O3J6ZIXK.js +82 -0
  22. package/dist/chunk-O3J6ZIXK.js.map +1 -0
  23. package/dist/chunk-RDQYDLYZ.js +69 -0
  24. package/dist/chunk-RDQYDLYZ.js.map +1 -0
  25. package/dist/chunk-WCQVDF3K.js +14 -0
  26. package/dist/cli.js +2713 -325
  27. package/dist/cli.js.map +1 -1
  28. package/dist/haiku-pruner-5KVT5AI2.js +8 -0
  29. package/dist/http-server-2ZQ6I43B.js +9 -0
  30. package/dist/index.d.ts +1886 -626
  31. package/dist/index.js +319 -46
  32. package/dist/index.js.map +1 -1
  33. package/dist/local-embedding-NZQTILGV.js +8 -0
  34. package/dist/mcp.d.ts +2 -0
  35. package/dist/mcp.js +386 -0
  36. package/dist/mcp.js.map +1 -0
  37. package/dist/openai-embedding-ZP5TSUJG.js +8 -0
  38. package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
  39. package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
  40. package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
  41. package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
  42. package/dist/plugin-IKQ6IRSJ.js +32 -0
  43. package/dist/plugin-IKQ6IRSJ.js.map +1 -0
  44. package/dist/resolve-ASGLBNUC.js +10 -0
  45. package/dist/resolve-ASGLBNUC.js.map +1 -0
  46. package/dist/stats-tui-AD3AMYGV.js +1904 -0
  47. package/dist/stats-tui-AD3AMYGV.js.map +1 -0
  48. package/package.json +38 -53
  49. package/src/brainbank.ts +617 -0
  50. package/src/cli/commands/collection.ts +77 -0
  51. package/src/cli/commands/context.ts +59 -0
  52. package/src/cli/commands/daemon.ts +100 -0
  53. package/src/cli/commands/docs.ts +71 -0
  54. package/src/cli/commands/files.ts +69 -0
  55. package/src/cli/commands/help.ts +82 -0
  56. package/src/cli/commands/index.ts +478 -0
  57. package/src/cli/commands/kv.ts +140 -0
  58. package/src/cli/commands/mcp-export.ts +273 -0
  59. package/src/cli/commands/mcp.ts +6 -0
  60. package/src/cli/commands/query.ts +167 -0
  61. package/src/cli/commands/reembed.ts +30 -0
  62. package/src/cli/commands/reindex.ts +40 -0
  63. package/src/cli/commands/scan.ts +336 -0
  64. package/src/cli/commands/search.ts +203 -0
  65. package/src/cli/commands/stats.ts +68 -0
  66. package/src/cli/commands/status.ts +47 -0
  67. package/src/cli/commands/watch.ts +47 -0
  68. package/src/cli/factory/brain-context.ts +43 -0
  69. package/src/cli/factory/builtin-registration.ts +87 -0
  70. package/src/cli/factory/config-loader.ts +77 -0
  71. package/src/cli/factory/index.ts +69 -0
  72. package/src/cli/factory/plugin-loader.ts +324 -0
  73. package/src/cli/index.ts +76 -0
  74. package/src/cli/server-client.ts +186 -0
  75. package/src/cli/tui/index-tui.tsx +667 -0
  76. package/src/cli/tui/stats-data.ts +523 -0
  77. package/src/cli/tui/stats-search.ts +262 -0
  78. package/src/cli/tui/stats-tui.tsx +1465 -0
  79. package/src/cli/tui/tree-scanner.ts +650 -0
  80. package/src/cli/utils.ts +137 -0
  81. package/src/config.ts +48 -0
  82. package/src/constants.ts +21 -0
  83. package/src/db/adapter.ts +112 -0
  84. package/src/db/metadata.ts +130 -0
  85. package/src/db/migrations.ts +66 -0
  86. package/src/db/sqlite-adapter.ts +218 -0
  87. package/src/db/tracker.ts +91 -0
  88. package/src/engine/index-api.ts +81 -0
  89. package/src/engine/reembed.ts +206 -0
  90. package/src/engine/search-api.ts +218 -0
  91. package/src/index.ts +150 -0
  92. package/src/lib/fts.ts +57 -0
  93. package/src/lib/languages.ts +179 -0
  94. package/src/lib/logger.ts +126 -0
  95. package/src/lib/math.ts +87 -0
  96. package/src/lib/provider-key.ts +20 -0
  97. package/src/lib/prune.ts +72 -0
  98. package/src/lib/rrf.ts +133 -0
  99. package/src/lib/write-lock.ts +108 -0
  100. package/src/mcp/mcp-server.ts +268 -0
  101. package/src/mcp/workspace-factory.ts +68 -0
  102. package/src/mcp/workspace-pool.ts +224 -0
  103. package/src/plugin.ts +381 -0
  104. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  105. package/src/providers/embeddings/embedding-worker.ts +141 -0
  106. package/src/providers/embeddings/local-embedding.ts +115 -0
  107. package/src/providers/embeddings/openai-embedding.ts +167 -0
  108. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  109. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  110. package/src/providers/embeddings/resolve.ts +34 -0
  111. package/src/providers/pruners/haiku-expander.ts +178 -0
  112. package/src/providers/pruners/haiku-pruner.ts +263 -0
  113. package/src/providers/vector/hnsw-index.ts +174 -0
  114. package/src/providers/vector/hnsw-loader.ts +129 -0
  115. package/src/search/bm25-boost.ts +76 -0
  116. package/src/search/context-builder.ts +209 -0
  117. package/src/search/keyword/composite-bm25-search.ts +47 -0
  118. package/src/search/query-decomposer.ts +124 -0
  119. package/src/search/types.ts +37 -0
  120. package/src/search/vector/composite-vector-search.ts +105 -0
  121. package/src/search/vector/mmr.ts +64 -0
  122. package/src/services/collection.ts +384 -0
  123. package/src/services/daemon.ts +87 -0
  124. package/src/services/http-server.ts +344 -0
  125. package/src/services/kv-service.ts +64 -0
  126. package/src/services/plugin-registry.ts +77 -0
  127. package/src/services/watch.ts +340 -0
  128. package/src/services/webhook-server.ts +100 -0
  129. package/src/types.ts +509 -0
  130. package/dist/chunk-2P3EGY6S.js +0 -37
  131. package/dist/chunk-2P3EGY6S.js.map +0 -1
  132. package/dist/chunk-3GAIDXRW.js +0 -105
  133. package/dist/chunk-3GAIDXRW.js.map +0 -1
  134. package/dist/chunk-4ZKBQ33J.js +0 -56
  135. package/dist/chunk-4ZKBQ33J.js.map +0 -1
  136. package/dist/chunk-7QVYU63E.js +0 -7
  137. package/dist/chunk-GOUBW7UA.js +0 -373
  138. package/dist/chunk-GOUBW7UA.js.map +0 -1
  139. package/dist/chunk-MJ3Y24H6.js +0 -185
  140. package/dist/chunk-MJ3Y24H6.js.map +0 -1
  141. package/dist/chunk-N6ZMBFDE.js +0 -224
  142. package/dist/chunk-N6ZMBFDE.js.map +0 -1
  143. package/dist/chunk-RAEBYV75.js +0 -709
  144. package/dist/chunk-RAEBYV75.js.map +0 -1
  145. package/dist/chunk-TW5NTYYZ.js +0 -2066
  146. package/dist/chunk-TW5NTYYZ.js.map +0 -1
  147. package/dist/chunk-Z5SU54HP.js +0 -171
  148. package/dist/chunk-Z5SU54HP.js.map +0 -1
  149. package/dist/code.d.ts +0 -31
  150. package/dist/code.js +0 -8
  151. package/dist/docs.d.ts +0 -19
  152. package/dist/docs.js +0 -8
  153. package/dist/git.d.ts +0 -31
  154. package/dist/git.js +0 -8
  155. package/dist/memory.d.ts +0 -19
  156. package/dist/memory.js +0 -146
  157. package/dist/memory.js.map +0 -1
  158. package/dist/notes.d.ts +0 -19
  159. package/dist/notes.js +0 -57
  160. package/dist/notes.js.map +0 -1
  161. package/dist/openai-PCTYLOWI.js +0 -8
  162. package/dist/types-Da_zLLOl.d.ts +0 -474
  163. /package/dist/{chunk-7QVYU63E.js.map → chunk-WCQVDF3K.js.map} +0 -0
  164. /package/dist/{code.js.map → haiku-pruner-5KVT5AI2.js.map} +0 -0
  165. /package/dist/{docs.js.map → http-server-2ZQ6I43B.js.map} +0 -0
  166. /package/dist/{git.js.map → local-embedding-NZQTILGV.js.map} +0 -0
  167. /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
- BrainBank
4
- } from "./chunk-TW5NTYYZ.js";
3
+ DEFAULT_PORT,
4
+ isServerRunning,
5
+ removePid
6
+ } from "./chunk-RDQYDLYZ.js";
5
7
  import {
6
- code
7
- } from "./chunk-RAEBYV75.js";
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
- git
10
- } from "./chunk-N6ZMBFDE.js";
11
- import {
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/integrations/cli.ts
21
- import * as path from "path";
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
- var c = {
24
- green: /* @__PURE__ */ __name((s) => `\x1B[32m${s}\x1B[0m`, "green"),
25
- red: /* @__PURE__ */ __name((s) => `\x1B[31m${s}\x1B[0m`, "red"),
26
- yellow: /* @__PURE__ */ __name((s) => `\x1B[33m${s}\x1B[0m`, "yellow"),
27
- cyan: /* @__PURE__ */ __name((s) => `\x1B[36m${s}\x1B[0m`, "cyan"),
28
- dim: /* @__PURE__ */ __name((s) => `\x1B[2m${s}\x1B[0m`, "dim"),
29
- bold: /* @__PURE__ */ __name((s) => `\x1B[1m${s}\x1B[0m`, "bold"),
30
- magenta: /* @__PURE__ */ __name((s) => `\x1B[35m${s}\x1B[0m`, "magenta")
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
- var args = process.argv.slice(2);
33
- var command = args[0];
34
- var subcommand = args[1];
35
- function getFlag(name) {
36
- const idx = args.indexOf(`--${name}`);
37
- return idx >= 0 ? args[idx + 1] : void 0;
38
- }
39
- __name(getFlag, "getFlag");
40
- function hasFlag(name) {
41
- return args.includes(`--${name}`);
42
- }
43
- __name(hasFlag, "hasFlag");
44
- var CONFIG_NAMES = [
45
- "config.ts",
46
- "config.js",
47
- "config.mjs"
48
- ];
49
- var INDEXER_EXTENSIONS = [".ts", ".js", ".mjs"];
50
- var _configCache = void 0;
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
- const files = fs.readdirSync(indexersDir).filter((f) => INDEXER_EXTENSIONS.some((ext) => f.endsWith(ext))).sort();
82
- const indexers = [];
83
- for (const file of files) {
84
- const filePath = path.join(indexersDir, file);
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 mod = await import(filePath);
87
- const indexer = mod.default ?? mod;
88
- if (indexer && typeof indexer === "object" && indexer.name) {
89
- indexers.push(indexer);
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.error(c.yellow(`\u26A0 ${file}: must export a default Indexer with a 'name' property, skipping`));
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 (builtins.includes("code")) brain.use(code({ repoPath: rp }));
135
- if (builtins.includes("git")) brain.use(git());
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
- if (builtins.includes("docs")) brain.use(docs());
138
- for (const indexer of folderIndexers) {
139
- brain.use(indexer);
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
- if (config?.indexers) {
142
- for (const indexer of config.indexers) {
143
- brain.use(indexer);
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
- return brain;
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(createBrain, "createBrain");
149
- function detectGitSubdirs(parentPath) {
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
- const entries = fs.readdirSync(parentPath, { withFileTypes: true });
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(detectGitSubdirs, "detectGitSubdirs");
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 repoPath = args[1] || ".";
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 modules = onlyRaw ? onlyRaw.split(",").map((s) => s.trim()) : void 0;
167
- if (docsPath && modules && !modules.includes("docs")) {
1584
+ const skipPrompt = hasFlag("yes") || hasFlag("y");
1585
+ const forceSetup = hasFlag("setup");
1586
+ const scan = scanRepo(repoPath);
1587
+ const configPlugins = scan.config.plugins ?? [];
1588
+ const externalDiscovery = await discoverExternalPlugins(repoPath, configPlugins);
1589
+ let externalPreviews = externalDiscovery.previews;
1590
+ if (externalDiscovery.modules.length > 0) {
1591
+ scan.modules = [...scan.modules, ...externalDiscovery.modules];
1592
+ }
1593
+ let modules;
1594
+ let tuiInclude = [];
1595
+ let tuiIgnore = [];
1596
+ let tuiConfig;
1597
+ if (onlyRaw) {
1598
+ printIndexHeader(scan, depth);
1599
+ modules = onlyRaw.split(",").map((s) => s.trim());
1600
+ } else if (scan.config.plugins && scan.config.plugins.length > 0 && !forceSetup) {
1601
+ printIndexHeader(scan, depth);
1602
+ modules = scan.config.plugins;
1603
+ } else if (skipPrompt) {
1604
+ printIndexHeader(scan, depth);
1605
+ modules = buildDefaultModules(scan);
1606
+ } else {
1607
+ const selection = await runIndexTui(scan, externalPreviews);
1608
+ if (!selection) {
1609
+ console.log(c.dim("\n Cancelled. Exiting.\n"));
1610
+ return;
1611
+ }
1612
+ modules = selection.modules;
1613
+ tuiInclude = selection.include;
1614
+ tuiIgnore = selection.ignore;
1615
+ tuiConfig = selection.config;
1616
+ if (modules.length === 0) {
1617
+ console.log(c.dim("\n Nothing selected. Exiting.\n"));
1618
+ return;
1619
+ }
1620
+ const oldPlugins = scan.config.plugins ?? [];
1621
+ const removed = oldPlugins.filter((p) => !modules.includes(p));
1622
+ if (removed.length > 0) {
1623
+ console.log(c.bold("\n\u2501\u2501\u2501 Deindexing \u2501\u2501\u2501\n"));
1624
+ for (const mod of removed) {
1625
+ console.log(` ${c.yellow("\u2717")} Removing ${mod} data...`);
1626
+ deindexModule(scan.repoPath, mod);
1627
+ console.log(` ${c.green("\u2713")} ${mod} data cleared`);
1628
+ }
1629
+ }
1630
+ console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2501\u2501\u2501\n"));
1631
+ console.log(" Selected modules:");
1632
+ for (const m of modules) {
1633
+ console.log(` ${c.green("\u2713")} ${m}`);
1634
+ }
1635
+ if (tuiInclude.length > 0) {
1636
+ console.log(` Include: ${c.cyan(tuiInclude.join(", "))}`);
1637
+ }
1638
+ if (tuiIgnore.length > 0) {
1639
+ console.log(` Ignore: ${c.yellow(tuiIgnore.join(", "))}`);
1640
+ }
1641
+ console.log("");
1642
+ }
1643
+ if (docsPath && !modules.includes("docs")) {
168
1644
  modules.push("docs");
169
1645
  }
170
- console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Index \u2501\u2501\u2501"));
171
- console.log(c.dim(` Repo: ${repoPath}`));
172
- console.log(c.dim(` Force: ${force}`));
173
- console.log(c.dim(` Git depth: ${depth}`));
174
- if (modules) console.log(c.dim(` Modules: ${modules.join(", ")}`));
175
- if (docsPath) console.log(c.dim(` Docs path: ${docsPath}`));
176
- const brain = await createBrain(repoPath);
177
- if (docsPath) {
178
- const absDocsPath = path.resolve(docsPath);
179
- const collName = path.basename(absDocsPath);
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
- await brain.addCollection({
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 result = await brain.index({
193
- modules,
194
- forceReindex: force,
195
- gitDepth: depth,
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
- const stats = brain.stats();
213
- console.log(`
214
- ${c.bold("Totals")}:`);
215
- if (stats.code) console.log(` Code chunks: ${stats.code.chunks}`);
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(cmdIndex, "cmdIndex");
1939
+ __name(deindexModule, "deindexModule");
1940
+
1941
+ // src/cli/commands/collection.ts
222
1942
  async function cmdCollection() {
223
- const sub = args[1];
1943
+ const pos = stripFlags(args);
1944
+ const sub = pos[1];
224
1945
  if (sub === "add") {
225
- const path2 = args[2];
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 (!path2 || !name) {
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
- await brain.addCollection({
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: path2,
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: ${path2} (${pattern})`));
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 collections = brain.listCollections();
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 = args[2];
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
- await brain.removeCollection(name);
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 sub = args[1];
2020
+ const pos = stripFlags(args);
2021
+ const sub = pos[1];
282
2022
  if (sub === "add") {
283
- const collName = args[2];
284
- const content = args.slice(3).filter((a) => !a.startsWith("--")).join(" ");
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 = args[2];
301
- const query = args.slice(3).filter((a) => !a.startsWith("--")).join(" ");
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 = args[2];
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 = args[2];
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 = args[2];
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 results = await brain.indexDocs(opts);
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
- console.log(` ${c.green(name)}: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
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).filter((a) => !a.startsWith("--")).join(" ");
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 results = await brain.searchDocs(query, { collection: collection ?? void 0, k });
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
- async function cmdContext() {
447
- const sub = args[1];
448
- if (sub === "add") {
449
- const collection = args[2];
450
- const path2 = args[3];
451
- const desc = args.slice(4).join(" ");
452
- if (!collection || !path2 || !desc) {
453
- console.log(c.red("Usage: brainbank context add <collection> <path> <description>"));
454
- process.exit(1);
455
- }
456
- const brain2 = await createBrain();
457
- await brain2.initialize();
458
- brain2.addContext(collection, path2, desc);
459
- console.log(c.green(`\u2713 Context added: ${collection}:${path2} \u2192 "${desc}"`));
460
- brain2.close();
461
- return;
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
- if (sub === "list") {
464
- const brain2 = await createBrain();
465
- await brain2.initialize();
466
- const contexts = brain2.listContexts();
467
- if (contexts.length === 0) {
468
- console.log(c.yellow(" No contexts configured."));
469
- } else {
470
- console.log(c.bold("\n\u2501\u2501\u2501 Contexts \u2501\u2501\u2501\n"));
471
- for (const ctx of contexts) {
472
- console.log(` ${c.cyan(ctx.collection)}:${ctx.path} \u2192 ${c.dim(ctx.context)}`);
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
- brain2.close();
476
- return;
2351
+ positional.push(args[i]);
477
2352
  }
478
- const task = args.slice(1).join(" ");
479
- if (!task) {
480
- console.log(c.red("Usage: brainbank context <task description>"));
481
- console.log(c.dim(" brainbank context add <collection> <path> <description>"));
482
- console.log(c.dim(" brainbank context list"));
483
- process.exit(1);
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
- const brain = await createBrain();
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(cmdContext, "cmdContext");
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 = args.slice(1).join(" ");
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
- const results = await brain.search(query);
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 = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
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
- const results = await brain.hybridSearch(query);
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 = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
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
- const results = brain.searchBM25(query);
535
- printResults(results);
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
- function printResults(results) {
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(" No results found."));
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 score = Math.round(r.score * 100);
546
- if (r.type === "code") {
547
- const m = r.metadata;
548
- console.log(`${c.green(`[CODE ${score}%]`)} ${c.bold(r.filePath)} \u2014 ${m.name || m.chunkType} ${c.dim(`L${m.startLine}-${m.endLine}`)}`);
549
- const preview = r.content.split("\n").slice(0, 5).join("\n");
550
- console.log(c.dim(preview));
551
- console.log("");
552
- } else if (r.type === "commit") {
553
- const m = r.metadata;
554
- console.log(`${c.cyan(`[COMMIT ${score}%]`)} ${c.bold(m.shortHash)} ${r.content} ${c.dim(`(${m.author})`)}`);
555
- if (m.files?.length) console.log(c.dim(` Files: ${m.files.slice(0, 4).join(", ")}`));
556
- console.log("");
557
- } else if (r.type === "document") {
558
- const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
559
- console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.metadata.collection}]${ctx}`);
560
- const preview = r.content.split("\n").slice(0, 4).join("\n");
561
- console.log(c.dim(preview));
562
- console.log("");
563
- }
564
- }
565
- }
566
- __name(printResults, "printResults");
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("Indexers")}: ${brain.indexers.join(", ")}
2761
+ console.log(` ${c.cyan("Plugins")}: ${brain.plugins.join(", ")}
573
2762
  `);
574
- if (s.code) {
575
- console.log(` ${c.cyan("Code")}`);
576
- console.log(` Files indexed: ${s.code.files}`);
577
- console.log(` Code chunks: ${s.code.chunks}`);
578
- console.log(` HNSW vectors: ${s.code.hnswSize}`);
579
- console.log("");
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
- if (result.code > 0) console.log(` ${c.green("\u2713")} Code: ${result.code} vectors`);
622
- if (result.git > 0) console.log(` ${c.green("\u2713")} Git: ${result.git} vectors`);
623
- if (result.docs > 0) console.log(` ${c.green("\u2713")} Docs: ${result.docs} vectors`);
624
- if (result.kv > 0) console.log(` ${c.green("\u2713")} KV: ${result.kv} vectors`);
625
- if (result.notes > 0) console.log(` ${c.green("\u2713")} Notes: ${result.notes} vectors`);
626
- if (result.memory > 0) console.log(` ${c.green("\u2713")} Memory: ${result.memory} vectors`);
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
- onIndex: /* @__PURE__ */ __name((file, indexer) => {
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(indexer)}: ${file}`);
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
- async function cmdServe() {
660
- await import("@brainbank/mcp");
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(cmdServe, "cmdServe");
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] Index code + git history`);
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("Context:"));
679
- console.log(` ${c.cyan("context")} <task> Get formatted context for a task`);
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("serve")} Start MCP server (stdio)`);
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>")} Context description`);
703
- console.log(` ${c.dim("--reranker <name>")} Reranker to use (qwen3)`);
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 context "add rate limiting to the API"'));
712
- console.log(c.dim(" brainbank serve"));
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 cmdServe();
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":