brainbank 0.7.0 → 0.8.0

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 (156) hide show
  1. package/README.md +76 -1398
  2. package/bin/brainbank +5 -1
  3. package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
  4. package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
  5. package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
  6. package/dist/chunk-3JZIM5AU.js.map +1 -0
  7. package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
  8. package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
  9. package/dist/chunk-7JDCHUJV.js +89 -0
  10. package/dist/chunk-7JDCHUJV.js.map +1 -0
  11. package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
  12. package/dist/chunk-7T2ZCZQA.js.map +1 -0
  13. package/dist/chunk-E3J37GDA.js +74 -0
  14. package/dist/chunk-E3J37GDA.js.map +1 -0
  15. package/dist/chunk-JEFWMS5Z.js +217 -0
  16. package/dist/chunk-JEFWMS5Z.js.map +1 -0
  17. package/dist/chunk-JRVYSTMP.js +3256 -0
  18. package/dist/chunk-JRVYSTMP.js.map +1 -0
  19. package/dist/chunk-OLDHLOMT.js +69 -0
  20. package/dist/chunk-OLDHLOMT.js.map +1 -0
  21. package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
  22. package/dist/chunk-QTZNB6AK.js.map +1 -0
  23. package/dist/chunk-RFF7HMP6.js +109 -0
  24. package/dist/chunk-RFF7HMP6.js.map +1 -0
  25. package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
  26. package/dist/chunk-TD3TEFI3.js.map +1 -0
  27. package/dist/cli.js +1036 -341
  28. package/dist/cli.js.map +1 -1
  29. package/dist/haiku-expander-WOVJIVXD.js +8 -0
  30. package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
  31. package/dist/http-server-GIRELCCL.js +9 -0
  32. package/dist/index.d.ts +1774 -611
  33. package/dist/index.js +282 -70
  34. package/dist/index.js.map +1 -1
  35. package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
  36. package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
  37. package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
  38. package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
  39. package/dist/plugin-FF4Q34TI.js +32 -0
  40. package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
  41. package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
  42. package/package.json +25 -52
  43. package/src/brainbank.ts +620 -0
  44. package/src/cli/commands/collection.ts +77 -0
  45. package/src/cli/commands/context.ts +171 -0
  46. package/src/cli/commands/daemon.ts +100 -0
  47. package/src/cli/commands/docs.ts +71 -0
  48. package/src/cli/commands/files.ts +69 -0
  49. package/src/cli/commands/help.ts +72 -0
  50. package/src/cli/commands/index.ts +282 -0
  51. package/src/cli/commands/kv.ts +140 -0
  52. package/src/cli/commands/mcp.ts +13 -0
  53. package/src/cli/commands/reembed.ts +30 -0
  54. package/src/cli/commands/scan.ts +365 -0
  55. package/src/cli/commands/search.ts +130 -0
  56. package/src/cli/commands/stats.ts +44 -0
  57. package/src/cli/commands/status.ts +47 -0
  58. package/src/cli/commands/watch.ts +43 -0
  59. package/src/cli/factory/brain-context.ts +43 -0
  60. package/src/cli/factory/builtin-registration.ts +123 -0
  61. package/src/cli/factory/config-loader.ts +72 -0
  62. package/src/cli/factory/index.ts +65 -0
  63. package/src/cli/factory/plugin-loader.ts +146 -0
  64. package/src/cli/index.ts +63 -0
  65. package/src/cli/server-client.ts +135 -0
  66. package/src/cli/utils.ts +121 -0
  67. package/src/config.ts +50 -0
  68. package/src/constants.ts +13 -0
  69. package/src/db/adapter.ts +112 -0
  70. package/src/db/metadata.ts +130 -0
  71. package/src/db/migrations.ts +66 -0
  72. package/src/db/sqlite-adapter.ts +208 -0
  73. package/src/db/tracker.ts +91 -0
  74. package/src/engine/index-api.ts +85 -0
  75. package/src/engine/reembed.ts +206 -0
  76. package/src/engine/search-api.ts +222 -0
  77. package/src/index.ts +159 -0
  78. package/src/lib/fts.ts +57 -0
  79. package/src/lib/languages.ts +180 -0
  80. package/src/lib/logger.ts +125 -0
  81. package/src/lib/math.ts +87 -0
  82. package/src/lib/provider-key.ts +20 -0
  83. package/src/lib/prune.ts +71 -0
  84. package/src/lib/rerank.ts +33 -0
  85. package/src/lib/rrf.ts +133 -0
  86. package/src/lib/write-lock.ts +108 -0
  87. package/src/plugin.ts +323 -0
  88. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  89. package/src/providers/embeddings/embedding-worker.ts +141 -0
  90. package/src/providers/embeddings/local-embedding.ts +115 -0
  91. package/src/providers/embeddings/openai-embedding.ts +167 -0
  92. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  93. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  94. package/src/providers/embeddings/resolve.ts +34 -0
  95. package/src/providers/pruners/haiku-expander.ts +152 -0
  96. package/src/providers/pruners/haiku-pruner.ts +112 -0
  97. package/src/providers/rerankers/qwen3-reranker.ts +180 -0
  98. package/src/providers/vector/hnsw-index.ts +174 -0
  99. package/src/providers/vector/hnsw-loader.ts +129 -0
  100. package/src/search/bm25-boost.ts +61 -0
  101. package/src/search/context-builder.ts +298 -0
  102. package/src/search/keyword/composite-bm25-search.ts +62 -0
  103. package/src/search/types.ts +35 -0
  104. package/src/search/vector/composite-vector-search.ts +76 -0
  105. package/src/search/vector/mmr.ts +64 -0
  106. package/src/services/collection.ts +405 -0
  107. package/src/services/daemon.ts +87 -0
  108. package/src/services/http-server.ts +288 -0
  109. package/src/services/kv-service.ts +65 -0
  110. package/src/services/plugin-registry.ts +109 -0
  111. package/src/services/watch.ts +348 -0
  112. package/src/services/webhook-server.ts +100 -0
  113. package/src/types.ts +504 -0
  114. package/dist/base-3SNc_CeY.d.ts +0 -593
  115. package/dist/chunk-424UFCY7.js.map +0 -1
  116. package/dist/chunk-7EZR47JV.js +0 -232
  117. package/dist/chunk-7EZR47JV.js.map +0 -1
  118. package/dist/chunk-B77KABWH.js.map +0 -1
  119. package/dist/chunk-CCXVL56V.js.map +0 -1
  120. package/dist/chunk-DI3H6JVZ.js +0 -2432
  121. package/dist/chunk-DI3H6JVZ.js.map +0 -1
  122. package/dist/chunk-FGL32LUJ.js +0 -754
  123. package/dist/chunk-FGL32LUJ.js.map +0 -1
  124. package/dist/chunk-JRSKWF6K.js +0 -313
  125. package/dist/chunk-JRSKWF6K.js.map +0 -1
  126. package/dist/chunk-U2Q2XGPZ.js +0 -42
  127. package/dist/chunk-U2Q2XGPZ.js.map +0 -1
  128. package/dist/chunk-VQ27YUHH.js +0 -629
  129. package/dist/chunk-VQ27YUHH.js.map +0 -1
  130. package/dist/chunk-VVXYZIIB.js +0 -304
  131. package/dist/chunk-VVXYZIIB.js.map +0 -1
  132. package/dist/chunk-YOLKSYWK.js +0 -79
  133. package/dist/chunk-YOLKSYWK.js.map +0 -1
  134. package/dist/chunk-ZNLN2VWV.js.map +0 -1
  135. package/dist/code.d.ts +0 -33
  136. package/dist/code.js +0 -9
  137. package/dist/docs.d.ts +0 -21
  138. package/dist/docs.js +0 -9
  139. package/dist/git.d.ts +0 -33
  140. package/dist/git.js +0 -9
  141. package/dist/memory.d.ts +0 -17
  142. package/dist/memory.js +0 -9
  143. package/dist/notes.d.ts +0 -17
  144. package/dist/notes.js +0 -10
  145. package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
  146. package/dist/resolve-CUJWY6HP.js.map +0 -1
  147. /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
  148. /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
  149. /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
  150. /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
  151. /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
  152. /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
  153. /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
  154. /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
  155. /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
  156. /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -1,314 +1,392 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- BrainBank
4
- } from "./chunk-DI3H6JVZ.js";
5
- import "./chunk-B77KABWH.js";
3
+ SUPPORTED_EXTENSIONS,
4
+ args,
5
+ c,
6
+ createBrain,
7
+ findDocsPlugin,
8
+ getConfig,
9
+ getFlag,
10
+ hasFlag,
11
+ isIgnoredDir,
12
+ isIgnoredFile,
13
+ loadConfig,
14
+ printResults,
15
+ registerConfigCollections,
16
+ stripFlags
17
+ } from "./chunk-JRVYSTMP.js";
6
18
  import {
7
- code
8
- } from "./chunk-FGL32LUJ.js";
9
- import {
10
- git
11
- } from "./chunk-JRSKWF6K.js";
12
- import {
13
- docs
14
- } from "./chunk-VQ27YUHH.js";
15
- import "./chunk-YOLKSYWK.js";
16
- import "./chunk-U2Q2XGPZ.js";
19
+ DEFAULT_PORT,
20
+ isServerRunning,
21
+ removePid
22
+ } from "./chunk-OLDHLOMT.js";
23
+ import "./chunk-E3J37GDA.js";
24
+ import "./chunk-7T2ZCZQA.js";
17
25
  import {
18
26
  __name
19
27
  } from "./chunk-7QVYU63E.js";
20
28
 
21
- // src/cli/utils.ts
22
- var c = {
23
- green: /* @__PURE__ */ __name((s) => `\x1B[32m${s}\x1B[0m`, "green"),
24
- red: /* @__PURE__ */ __name((s) => `\x1B[31m${s}\x1B[0m`, "red"),
25
- yellow: /* @__PURE__ */ __name((s) => `\x1B[33m${s}\x1B[0m`, "yellow"),
26
- cyan: /* @__PURE__ */ __name((s) => `\x1B[36m${s}\x1B[0m`, "cyan"),
27
- dim: /* @__PURE__ */ __name((s) => `\x1B[2m${s}\x1B[0m`, "dim"),
28
- bold: /* @__PURE__ */ __name((s) => `\x1B[1m${s}\x1B[0m`, "bold"),
29
- magenta: /* @__PURE__ */ __name((s) => `\x1B[35m${s}\x1B[0m`, "magenta")
30
- };
31
- var args = process.argv.slice(2);
32
- function getFlag(name) {
33
- const idx = args.indexOf(`--${name}`);
34
- return idx >= 0 ? args[idx + 1] : void 0;
35
- }
36
- __name(getFlag, "getFlag");
37
- function hasFlag(name) {
38
- return args.includes(`--${name}`);
39
- }
40
- __name(hasFlag, "hasFlag");
41
- var VALUE_FLAGS = /* @__PURE__ */ new Set([
42
- "repo",
43
- "depth",
44
- "collection",
45
- "pattern",
46
- "context",
47
- "name",
48
- "keep",
49
- "reranker",
50
- "only",
51
- "docs",
52
- "ignore",
53
- "meta",
54
- "k",
55
- "mode",
56
- "limit"
57
- ]);
58
- function stripFlags(argv) {
59
- const result = [];
60
- for (let i = 0; i < argv.length; i++) {
61
- if (argv[i].startsWith("--")) {
62
- const name = argv[i].slice(2);
63
- if (VALUE_FLAGS.has(name)) i++;
64
- continue;
65
- }
66
- result.push(argv[i]);
67
- }
68
- return result;
69
- }
70
- __name(stripFlags, "stripFlags");
71
- function printResults(results) {
72
- if (results.length === 0) {
73
- console.log(c.yellow(" No results found."));
74
- return;
75
- }
76
- for (const r of results) {
77
- const score = Math.round(r.score * 100);
78
- if (r.type === "code") {
79
- const m = r.metadata;
80
- console.log(
81
- `${c.green(`[CODE ${score}%]`)} ${c.bold(r.filePath)} \u2014 ${m.name || m.chunkType} ${c.dim(`L${m.startLine}-${m.endLine}`)}`
82
- );
83
- console.log(c.dim(r.content.split("\n").slice(0, 5).join("\n")));
84
- console.log("");
85
- } else if (r.type === "commit") {
86
- const m = r.metadata;
87
- console.log(
88
- `${c.cyan(`[COMMIT ${score}%]`)} ${c.bold(m.shortHash)} ${r.content} ${c.dim(`(${m.author})`)}`
89
- );
90
- if (m.files?.length) console.log(c.dim(` Files: ${m.files.slice(0, 4).join(", ")}`));
91
- console.log("");
92
- } else if (r.type === "document") {
93
- const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
94
- console.log(
95
- `${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.metadata.collection}]${ctx}`
96
- );
97
- console.log(c.dim(r.content.split("\n").slice(0, 4).join("\n")));
98
- console.log("");
99
- }
100
- }
101
- }
102
- __name(printResults, "printResults");
103
-
104
- // src/cli/commands/index-cmd.ts
29
+ // src/cli/commands/index.ts
30
+ import * as fs2 from "fs";
105
31
  import * as path2 from "path";
106
32
 
107
- // src/cli/factory.ts
108
- import * as path from "path";
33
+ // src/cli/commands/scan.ts
109
34
  import * as fs from "fs";
110
- var CONFIG_NAMES = ["config.json", "config.ts", "config.js", "config.mjs"];
111
- var INDEXER_EXTENSIONS = [".ts", ".js", ".mjs"];
112
- var NOT_LOADED = /* @__PURE__ */ Symbol("not-loaded");
113
- var _configCache = NOT_LOADED;
114
- var _folderPluginsCache = NOT_LOADED;
115
- async function loadConfig() {
116
- if (_configCache !== NOT_LOADED) return _configCache;
117
- const repoPath = getFlag("repo") ?? ".";
118
- const brainbankDir = path.resolve(repoPath, ".brainbank");
119
- for (const name of CONFIG_NAMES) {
120
- const configPath = path.join(brainbankDir, name);
121
- if (!fs.existsSync(configPath)) continue;
35
+ import * as path from "path";
36
+ import { execSync } from "child_process";
37
+ function scanRepo(repoPath) {
38
+ const resolved = path.resolve(repoPath);
39
+ const gitSubdirs = scanGitSubdirs(resolved);
40
+ return {
41
+ repoPath: resolved,
42
+ modules: scanModules(resolved, gitSubdirs),
43
+ config: scanConfig(resolved),
44
+ db: scanDb(resolved),
45
+ gitSubdirs
46
+ };
47
+ }
48
+ __name(scanRepo, "scanRepo");
49
+ function scanModules(repoPath, gitSubdirs) {
50
+ return [
51
+ scanCodeModule(repoPath),
52
+ scanGitModule(repoPath, gitSubdirs),
53
+ scanDocsModule(repoPath)
54
+ ];
55
+ }
56
+ __name(scanModules, "scanModules");
57
+ function scanCodeModule(repoPath) {
58
+ const byLanguage = /* @__PURE__ */ new Map();
59
+ let total = 0;
60
+ function walk(dir) {
61
+ let entries;
122
62
  try {
123
- if (name === "config.json") {
124
- const raw = fs.readFileSync(configPath, "utf-8");
125
- _configCache = JSON.parse(raw);
126
- } else {
127
- const mod = await import(configPath);
128
- _configCache = mod.default ?? mod;
63
+ entries = fs.readdirSync(dir, { withFileTypes: true });
64
+ } catch {
65
+ return;
66
+ }
67
+ for (const entry of entries) {
68
+ const fullPath = path.join(dir, entry.name);
69
+ const isDir = entry.isDirectory() || entry.isSymbolicLink() && (() => {
70
+ try {
71
+ return fs.statSync(fullPath).isDirectory();
72
+ } catch {
73
+ return false;
74
+ }
75
+ })();
76
+ if (isDir) {
77
+ if (isIgnoredDir(entry.name)) continue;
78
+ walk(fullPath);
79
+ } else if (entry.isFile()) {
80
+ if (isIgnoredFile(entry.name)) continue;
81
+ const ext = path.extname(entry.name).toLowerCase();
82
+ const lang = SUPPORTED_EXTENSIONS[ext];
83
+ if (!lang) continue;
84
+ byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
85
+ total++;
129
86
  }
130
- return _configCache;
131
- } catch (err) {
132
- console.error(c.red(`Error loading .brainbank/${name}: ${err.message}`));
133
- process.exit(1);
134
87
  }
135
88
  }
136
- _configCache = null;
137
- return null;
89
+ __name(walk, "walk");
90
+ walk(repoPath);
91
+ if (total === 0) {
92
+ return { name: "code", available: false, summary: "no supported source files found", icon: "\u{1F4C1}", checked: false, disabled: "nothing to index" };
93
+ }
94
+ const langCount = byLanguage.size;
95
+ const sorted = [...byLanguage.entries()].sort((a, b) => b[1] - a[1]);
96
+ const maxShow = 7;
97
+ const shown = sorted.slice(0, maxShow);
98
+ const remaining = sorted.length - maxShow;
99
+ const details = [];
100
+ for (let i = 0; i < shown.length; i++) {
101
+ const [lang, count] = shown[i];
102
+ const isLast = i === shown.length - 1 && remaining <= 0;
103
+ const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
104
+ details.push(`${prefix} ${lang.padEnd(14)} ${count} files`);
105
+ }
106
+ if (remaining > 0) {
107
+ details.push(`\u2514\u2500\u2500 ...and ${remaining} more`);
108
+ }
109
+ return {
110
+ name: "code",
111
+ available: true,
112
+ summary: `${total} files (${langCount} language${langCount > 1 ? "s" : ""})`,
113
+ icon: "\u{1F4C1}",
114
+ checked: true,
115
+ details
116
+ };
138
117
  }
139
- __name(loadConfig, "loadConfig");
140
- async function getConfig() {
141
- return loadConfig();
118
+ __name(scanCodeModule, "scanCodeModule");
119
+ function scanGitModule(repoPath, gitSubdirs) {
120
+ const stats = scanGitStats(repoPath, gitSubdirs);
121
+ if (!stats) {
122
+ return { name: "git", available: false, summary: "no .git directory found", icon: "\u{1F4DC}", checked: false, disabled: "not a git repo" };
123
+ }
124
+ const details = [];
125
+ if (stats.lastMessage) {
126
+ details.push(`Last: ${stats.lastMessage} (${stats.lastDate})`);
127
+ }
128
+ return {
129
+ name: "git",
130
+ available: true,
131
+ summary: `${stats.commitCount.toLocaleString()} commits`,
132
+ icon: "\u{1F4DC}",
133
+ checked: true,
134
+ details
135
+ };
142
136
  }
143
- __name(getConfig, "getConfig");
144
- async function resolveEmbeddingKey(key) {
145
- const { resolveEmbedding } = await import("./resolve-CUJWY6HP.js");
146
- return resolveEmbedding(key);
137
+ __name(scanGitModule, "scanGitModule");
138
+ function scanDocsModule(repoPath) {
139
+ const collections = scanDocsCollections(repoPath);
140
+ if (collections.length === 0) {
141
+ return { name: "docs", available: false, summary: "no documents found", icon: "\u{1F4C4}", checked: false, disabled: "no .md/.mdx files" };
142
+ }
143
+ const totalFiles = collections.reduce((s, d) => s + d.fileCount, 0);
144
+ const details = collections.map((d, i) => {
145
+ const isLast = i === collections.length - 1;
146
+ const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
147
+ return `${prefix} ${d.name.padEnd(10)} \u2192 ${d.path} (${d.fileCount} files)`;
148
+ });
149
+ return {
150
+ name: "docs",
151
+ available: true,
152
+ summary: `${collections.length} collection${collections.length > 1 ? "s" : ""} (${totalFiles} files)`,
153
+ icon: "\u{1F4C4}",
154
+ checked: true,
155
+ details
156
+ };
147
157
  }
148
- __name(resolveEmbeddingKey, "resolveEmbeddingKey");
149
- async function discoverFolderPlugins() {
150
- if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
151
- const repoPath = getFlag("repo") ?? ".";
152
- const indexersDir = path.resolve(repoPath, ".brainbank", "indexers");
153
- if (!fs.existsSync(indexersDir)) {
154
- _folderPluginsCache = [];
155
- return [];
158
+ __name(scanDocsModule, "scanDocsModule");
159
+ function scanGitStats(repoPath, gitSubdirs) {
160
+ if (fs.existsSync(path.join(repoPath, ".git"))) {
161
+ return gitStats(repoPath);
156
162
  }
157
- const files = fs.readdirSync(indexersDir).filter((f) => INDEXER_EXTENSIONS.some((ext) => f.endsWith(ext))).sort();
158
- const indexers = [];
159
- for (const file of files) {
160
- const filePath = path.join(indexersDir, file);
161
- try {
162
- const mod = await import(filePath);
163
- const indexer = mod.default ?? mod;
164
- if (indexer && typeof indexer === "object" && indexer.name) {
165
- indexers.push(indexer);
166
- } else {
167
- console.error(c.yellow(`\u26A0 ${file}: must export a default Plugin with a 'name' property, skipping`));
163
+ if (gitSubdirs.length === 0) return null;
164
+ let totalCommits = 0;
165
+ let latestMessage = "";
166
+ let latestDate = "";
167
+ for (const sub of gitSubdirs) {
168
+ const stats = gitStats(path.join(repoPath, sub.name));
169
+ if (stats) {
170
+ totalCommits += stats.commitCount;
171
+ if (!latestMessage) {
172
+ latestMessage = stats.lastMessage;
173
+ latestDate = stats.lastDate;
168
174
  }
169
- } catch (err) {
170
- console.error(c.red(`Error loading indexer ${file}: ${err.message}`));
171
175
  }
172
176
  }
173
- _folderPluginsCache = indexers;
174
- return indexers;
177
+ return totalCommits > 0 ? { commitCount: totalCommits, lastMessage: latestMessage, lastDate: latestDate } : null;
175
178
  }
176
- __name(discoverFolderPlugins, "discoverFolderPlugins");
177
- function detectGitSubdirs(parentPath) {
179
+ __name(scanGitStats, "scanGitStats");
180
+ function gitStats(dir) {
178
181
  try {
179
- const entries = fs.readdirSync(parentPath, { withFileTypes: true });
180
- return entries.filter(
181
- (e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("node_modules") && fs.existsSync(path.join(parentPath, e.name, ".git"))
182
- ).map((e) => ({ name: e.name, path: path.join(parentPath, e.name) }));
182
+ const count = parseInt(execSync("git rev-list --count HEAD", { cwd: dir, encoding: "utf-8" }).trim(), 10);
183
+ const log = execSync('git log -1 --format="%s|%ar"', { cwd: dir, encoding: "utf-8" }).trim();
184
+ const [lastMessage, lastDate] = log.split("|");
185
+ return { commitCount: count, lastMessage: lastMessage ?? "", lastDate: lastDate ?? "" };
183
186
  } catch {
184
- return [];
187
+ return null;
185
188
  }
186
189
  }
187
- __name(detectGitSubdirs, "detectGitSubdirs");
188
- async function createBrain(repoPath) {
189
- const rp = repoPath ?? getFlag("repo") ?? ".";
190
- const config = await loadConfig();
191
- const folderIndexers = await discoverFolderPlugins();
192
- const brainOpts = { repoPath: rp, ...config?.brainbank ?? {} };
193
- if (config?.maxFileSize) brainOpts.maxFileSize = config.maxFileSize;
194
- await setupProviders(brainOpts, config);
195
- const brain = new BrainBank(brainOpts);
196
- const builtins = config?.plugins ?? config?.builtins ?? ["code", "git", "docs"];
197
- await registerBuiltins(brain, rp, builtins, config);
198
- for (const indexer of folderIndexers) brain.use(indexer);
199
- if (config?.indexers) {
200
- for (const indexer of config.indexers) brain.use(indexer);
201
- }
202
- return brain;
203
- }
204
- __name(createBrain, "createBrain");
205
- async function setupProviders(brainOpts, config) {
206
- const rerankerFlag = getFlag("reranker") ?? config?.reranker;
207
- if (rerankerFlag === "qwen3") {
208
- const { Qwen3Reranker } = await import("./qwen3-reranker-3MHEENT5.js");
209
- brainOpts.reranker = new Qwen3Reranker();
210
- }
211
- const embFlag = getFlag("embedding") ?? config?.embedding;
212
- if (embFlag) {
213
- const provider = await resolveEmbeddingKey(embFlag);
214
- brainOpts.embeddingProvider = provider;
215
- brainOpts.embeddingDims = provider.dims;
216
- }
217
- }
218
- __name(setupProviders, "setupProviders");
219
- async function registerBuiltins(brain, rp, builtins, config) {
220
- const resolvedRp = path.resolve(rp);
221
- const hasRootGit = fs.existsSync(path.join(resolvedRp, ".git"));
222
- const gitSubdirs = !hasRootGit ? detectGitSubdirs(resolvedRp) : [];
223
- const codeEmb = config?.code?.embedding ? await resolveEmbeddingKey(config.code.embedding) : void 0;
224
- const gitEmb = config?.git?.embedding ? await resolveEmbeddingKey(config.git.embedding) : void 0;
225
- const docsEmb = config?.docs?.embedding ? await resolveEmbeddingKey(config.docs.embedding) : void 0;
226
- if (gitSubdirs.length > 0 && (builtins.includes("code") || builtins.includes("git"))) {
227
- console.log(c.cyan(` Multi-repo: found ${gitSubdirs.length} git repos: ${gitSubdirs.map((d) => d.name).join(", ")}`));
228
- for (const sub of gitSubdirs) {
229
- if (builtins.includes("code")) {
230
- brain.use(code({
231
- repoPath: sub.path,
232
- name: `code:${sub.name}`,
233
- embeddingProvider: codeEmb,
234
- maxFileSize: config?.code?.maxFileSize
235
- }));
190
+ __name(gitStats, "gitStats");
191
+ function scanDocsCollections(repoPath) {
192
+ const results = [];
193
+ const seen = /* @__PURE__ */ new Set();
194
+ const configPath = path.join(repoPath, ".brainbank", "config.json");
195
+ try {
196
+ if (fs.existsSync(configPath)) {
197
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
198
+ const docsCfg = config?.docs;
199
+ const collections = docsCfg?.collections;
200
+ if (collections) {
201
+ for (const coll of collections) {
202
+ const absPath = path.resolve(repoPath, coll.path);
203
+ results.push({ name: coll.name, path: coll.path, fileCount: countDocs(absPath) });
204
+ seen.add(absPath);
205
+ }
236
206
  }
237
- if (builtins.includes("git")) {
238
- brain.use(git({
239
- repoPath: sub.path,
240
- name: `git:${sub.name}`,
241
- embeddingProvider: gitEmb,
242
- depth: config?.git?.depth,
243
- maxDiffBytes: config?.git?.maxDiffBytes
244
- }));
207
+ }
208
+ } catch {
209
+ }
210
+ const rootDocs = countDocsShallow(repoPath);
211
+ if (rootDocs > 0) {
212
+ results.push({ name: "(root)", path: ".", fileCount: rootDocs });
213
+ }
214
+ try {
215
+ for (const entry of fs.readdirSync(repoPath, { withFileTypes: true })) {
216
+ if (!entry.isDirectory()) continue;
217
+ if (isIgnoredDir(entry.name)) continue;
218
+ if (entry.name.startsWith(".")) continue;
219
+ const dirPath = path.join(repoPath, entry.name);
220
+ if (seen.has(dirPath)) continue;
221
+ const count = countDocs(dirPath);
222
+ if (count > 0) {
223
+ results.push({ name: entry.name, path: `./${entry.name}`, fileCount: count });
245
224
  }
246
225
  }
247
- } else {
248
- if (builtins.includes("code")) {
249
- brain.use(code({
250
- repoPath: rp,
251
- embeddingProvider: codeEmb,
252
- maxFileSize: config?.code?.maxFileSize
253
- }));
254
- }
255
- if (builtins.includes("git")) {
256
- brain.use(git({
257
- embeddingProvider: gitEmb,
258
- depth: config?.git?.depth,
259
- maxDiffBytes: config?.git?.maxDiffBytes
260
- }));
261
- }
262
- }
263
- if (builtins.includes("docs")) {
264
- brain.use(docs({ embeddingProvider: docsEmb }));
265
- }
266
- }
267
- __name(registerBuiltins, "registerBuiltins");
268
- async function registerConfigCollections(brain, config) {
269
- const collections = config?.docs?.collections;
270
- if (!collections?.length) return;
271
- for (const coll of collections) {
272
- const absPath = path.resolve(coll.path);
273
- try {
274
- await brain.addCollection({
275
- name: coll.name,
276
- path: absPath,
277
- pattern: coll.pattern ?? "**/*.md",
278
- ignore: coll.ignore,
279
- context: coll.context
280
- });
281
- } catch {
226
+ } catch {
227
+ }
228
+ return results;
229
+ }
230
+ __name(scanDocsCollections, "scanDocsCollections");
231
+ function countDocs(dir) {
232
+ let count = 0;
233
+ try {
234
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
235
+ const ePath = path.join(dir, e.name);
236
+ const isDir = e.isDirectory() || e.isSymbolicLink() && (() => {
237
+ try {
238
+ return fs.statSync(ePath).isDirectory();
239
+ } catch {
240
+ return false;
241
+ }
242
+ })();
243
+ if (isDir) {
244
+ if (isIgnoredDir(e.name)) continue;
245
+ count += countDocs(ePath);
246
+ } else if ((e.isFile() || e.isSymbolicLink()) && /\.mdx?$/i.test(e.name)) {
247
+ count++;
248
+ }
249
+ }
250
+ } catch {
251
+ }
252
+ return count;
253
+ }
254
+ __name(countDocs, "countDocs");
255
+ function countDocsShallow(dir) {
256
+ let count = 0;
257
+ try {
258
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
259
+ if (e.isFile() && /\.mdx?$/i.test(e.name)) count++;
282
260
  }
261
+ } catch {
262
+ }
263
+ return count;
264
+ }
265
+ __name(countDocsShallow, "countDocsShallow");
266
+ function scanConfig(repoPath) {
267
+ const configPath = path.join(repoPath, ".brainbank", "config.json");
268
+ if (!fs.existsSync(configPath)) return { exists: false };
269
+ try {
270
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
271
+ const codeCfg = config?.code;
272
+ return {
273
+ exists: true,
274
+ ignore: codeCfg?.ignore,
275
+ plugins: config?.plugins
276
+ };
277
+ } catch {
278
+ return { exists: false };
283
279
  }
284
280
  }
285
- __name(registerConfigCollections, "registerConfigCollections");
281
+ __name(scanConfig, "scanConfig");
282
+ function scanDb(repoPath) {
283
+ const dbPath = path.join(repoPath, ".brainbank", "data", "brainbank.db");
284
+ if (!fs.existsSync(dbPath)) return { exists: false, sizeMB: 0 };
285
+ try {
286
+ const stat = fs.statSync(dbPath);
287
+ return {
288
+ exists: true,
289
+ sizeMB: Math.round(stat.size / 1024 / 1024 * 10) / 10,
290
+ lastModified: stat.mtime
291
+ };
292
+ } catch {
293
+ return { exists: false, sizeMB: 0 };
294
+ }
295
+ }
296
+ __name(scanDb, "scanDb");
297
+ function scanGitSubdirs(repoPath) {
298
+ if (fs.existsSync(path.join(repoPath, ".git"))) return [];
299
+ try {
300
+ let subdirs = fs.readdirSync(repoPath, { withFileTypes: true }).filter((e) => {
301
+ if (e.name.startsWith(".")) return false;
302
+ const isDir = e.isDirectory() || e.isSymbolicLink() && (() => {
303
+ try {
304
+ return fs.statSync(path.join(repoPath, e.name)).isDirectory();
305
+ } catch {
306
+ return false;
307
+ }
308
+ })();
309
+ return isDir;
310
+ }).filter((e) => fs.existsSync(path.join(repoPath, e.name, ".git"))).map((e) => ({ name: e.name }));
311
+ const configRepos = readReposFromConfig(repoPath);
312
+ if (configRepos) {
313
+ subdirs = subdirs.filter((s) => configRepos.includes(s.name));
314
+ }
315
+ return subdirs;
316
+ } catch {
317
+ return [];
318
+ }
319
+ }
320
+ __name(scanGitSubdirs, "scanGitSubdirs");
321
+ function readReposFromConfig(repoPath) {
322
+ const configPath = path.join(repoPath, ".brainbank", "config.json");
323
+ try {
324
+ if (!fs.existsSync(configPath)) return null;
325
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
326
+ const repos = config.repos;
327
+ if (Array.isArray(repos) && repos.every((r) => typeof r === "string")) {
328
+ return repos;
329
+ }
330
+ return null;
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ __name(readReposFromConfig, "readReposFromConfig");
286
336
 
287
- // src/cli/commands/index-cmd.ts
337
+ // src/cli/commands/index.ts
288
338
  async function cmdIndex() {
289
- const repoPath = args[1] || ".";
339
+ const positional = stripFlags(args);
340
+ const repoPath = positional[1] || ".";
290
341
  const force = hasFlag("force");
291
342
  const depth = parseInt(getFlag("depth") || "500", 10);
292
343
  const onlyRaw = getFlag("only");
293
344
  const docsPath = getFlag("docs");
294
- const modules = onlyRaw ? onlyRaw.split(",").map((s) => s.trim()) : void 0;
295
- if (docsPath && modules && !modules.includes("docs")) {
345
+ const skipPrompt = hasFlag("yes") || hasFlag("y");
346
+ const scan = scanRepo(repoPath);
347
+ printScanTree(scan, depth);
348
+ let modules;
349
+ if (onlyRaw) {
350
+ modules = onlyRaw.split(",").map((s) => s.trim());
351
+ } else if (scan.config.plugins && scan.config.plugins.length > 0) {
352
+ modules = scan.config.plugins;
353
+ console.log(c.dim(`
354
+ Using config: plugins = [${modules.join(", ")}]
355
+ `));
356
+ } else if (skipPrompt) {
357
+ modules = buildDefaultModules(scan);
358
+ } else {
359
+ modules = await promptModules(scan);
360
+ if (modules.length === 0) {
361
+ console.log(c.dim("\n Nothing selected. Exiting.\n"));
362
+ return;
363
+ }
364
+ console.clear();
365
+ console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2501\u2501\u2501\n"));
366
+ console.log(" Selected modules:");
367
+ for (const m of modules) {
368
+ console.log(` ${c.green("\u2713")} ${m}`);
369
+ }
370
+ console.log("");
371
+ }
372
+ if (docsPath && !modules.includes("docs")) {
296
373
  modules.push("docs");
297
374
  }
298
- console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Index \u2501\u2501\u2501"));
299
- console.log(c.dim(` Repo: ${repoPath}`));
300
- console.log(c.dim(` Force: ${force}`));
301
- console.log(c.dim(` Git depth: ${depth}`));
302
- if (modules) console.log(c.dim(` Modules: ${modules.join(", ")}`));
303
- if (docsPath) console.log(c.dim(` Docs path: ${docsPath}`));
375
+ if (!scan.config.exists && !skipPrompt) {
376
+ await saveConfig(scan.repoPath, modules);
377
+ }
378
+ console.log(c.bold(`
379
+ \u2501\u2501\u2501 Indexing: ${modules.join(", ")} \u2501\u2501\u2501`));
304
380
  const brain = await createBrain(repoPath);
305
- const config = await getConfig();
306
- await registerConfigCollections(brain, config);
381
+ await brain.initialize();
382
+ const config = await getConfig(repoPath);
383
+ await registerConfigCollections(brain, repoPath, config);
307
384
  if (docsPath) {
308
385
  const absDocsPath = path2.resolve(docsPath);
309
386
  const collName = path2.basename(absDocsPath);
310
387
  try {
311
- await brain.addCollection({
388
+ const docsPlugin = findDocsPlugin(brain);
389
+ await docsPlugin?.addCollection({
312
390
  name: collName,
313
391
  path: absDocsPath,
314
392
  pattern: "**/*.md",
@@ -322,33 +400,163 @@ async function cmdIndex() {
322
400
  const result = await brain.index({
323
401
  modules,
324
402
  forceReindex: force,
325
- gitDepth: depth,
403
+ pluginOptions: { depth },
326
404
  onProgress: /* @__PURE__ */ __name((stage, msg) => {
327
405
  process.stdout.write(`\r ${c.cyan(stage.toUpperCase())} ${msg} `);
328
406
  }, "onProgress")
329
407
  });
330
408
  console.log("\n");
331
- if (result.code) {
332
- console.log(` ${c.green("Code")}: ${result.code.indexed} indexed, ${result.code.skipped} skipped, ${result.code.chunks ?? 0} chunks`);
333
- }
334
- if (result.git) {
335
- console.log(` ${c.green("Git")}: ${result.git.indexed} indexed, ${result.git.skipped} skipped`);
336
- }
337
- if (result.docs) {
338
- for (const [name, stat] of Object.entries(result.docs)) {
339
- console.log(` ${c.green("Docs")}: [${name}] ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
409
+ for (const [name, value] of Object.entries(result)) {
410
+ if (!value) continue;
411
+ const v = value;
412
+ if (typeof v.indexed === "number") {
413
+ const parts = [`${v.indexed} indexed`, `${v.skipped ?? 0} skipped`];
414
+ if (typeof v.chunks === "number") parts.push(`${v.chunks} chunks`);
415
+ if (typeof v.removed === "number" && v.removed > 0) parts.push(`${v.removed} removed`);
416
+ console.log(` ${c.green(name)}: ${parts.join(", ")}`);
417
+ } else {
418
+ console.log(` ${c.green(name)}: done`);
340
419
  }
341
420
  }
342
421
  const stats = brain.stats();
343
422
  console.log(`
344
423
  ${c.bold("Totals")}:`);
345
- if (stats.code) console.log(` Code chunks: ${stats.code.chunks}`);
346
- if (stats.git) console.log(` Git commits: ${stats.git.commits}`);
347
- if (stats.git) console.log(` Co-edit pairs: ${stats.git.coEdits}`);
348
- if (stats.documents) console.log(` Documents: ${stats.documents.documents}`);
424
+ for (const [name, s] of Object.entries(stats)) {
425
+ if (!s || typeof s !== "object") continue;
426
+ const entries = Object.entries(s).map(([k, v]) => `${k}: ${v}`).join(", ");
427
+ console.log(` ${name}: ${entries}`);
428
+ }
349
429
  brain.close();
350
430
  }
351
431
  __name(cmdIndex, "cmdIndex");
432
+ function printScanTree(scan, depth) {
433
+ console.clear();
434
+ console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Scan \u2501\u2501\u2501"));
435
+ console.log(c.dim(` Repo: ${scan.repoPath}`));
436
+ if (scan.gitSubdirs.length > 0) {
437
+ console.log(c.cyan(`
438
+ \u{1F500} Multi-repo \u2014 ${scan.gitSubdirs.length} git repos detected`));
439
+ for (const sub of scan.gitSubdirs) {
440
+ console.log(c.dim(` \u2514\u2500\u2500 ${sub.name}`));
441
+ }
442
+ }
443
+ for (const mod of scan.modules) {
444
+ console.log("");
445
+ if (mod.available) {
446
+ const extra = mod.name === "git" ? ` (depth: ${depth})` : "";
447
+ console.log(` ${mod.icon} ${c.bold(capitalizeFirst(mod.name))} \u2014 ${mod.summary}${extra}`);
448
+ if (mod.details) {
449
+ for (const d of mod.details) {
450
+ console.log(c.dim(` ${d}`));
451
+ }
452
+ }
453
+ } else {
454
+ console.log(` ${mod.icon} ${c.dim(`${capitalizeFirst(mod.name)} \u2014 ${mod.summary}`)}`);
455
+ }
456
+ }
457
+ if (scan.config.ignore?.length) {
458
+ console.log(c.dim(` Ignore: ${scan.config.ignore.join(", ")}`));
459
+ }
460
+ console.log("");
461
+ if (scan.config.exists) {
462
+ console.log(` \u2699\uFE0F ${c.dim("Config:")} .brainbank/config.json ${c.green("\u2713")}`);
463
+ }
464
+ if (scan.db?.exists) {
465
+ const ago = scan.db.lastModified ? timeSince(scan.db.lastModified) : "";
466
+ console.log(` \u{1F4BE} ${c.dim("DB:")} ${scan.db.sizeMB} MB${ago ? `, last indexed ${ago}` : ""}`);
467
+ } else {
468
+ console.log(` \u{1F4BE} ${c.dim("DB: new (first index)")}`);
469
+ }
470
+ console.log("");
471
+ }
472
+ __name(printScanTree, "printScanTree");
473
+ function buildDefaultModules(scan) {
474
+ return scan.modules.filter((m) => m.available && m.checked).map((m) => m.name);
475
+ }
476
+ __name(buildDefaultModules, "buildDefaultModules");
477
+ async function promptModules(scan) {
478
+ const { checkbox } = await import("@inquirer/prompts");
479
+ console.log(c.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
480
+ const choices = scan.modules.map((m) => ({
481
+ name: `${capitalizeFirst(m.name).padEnd(6)} \u2014 ${m.summary}`,
482
+ value: m.name,
483
+ checked: m.checked && m.available,
484
+ disabled: m.available ? void 0 : m.disabled
485
+ }));
486
+ return checkbox({
487
+ message: "Select modules to index:\n",
488
+ choices
489
+ });
490
+ }
491
+ __name(promptModules, "promptModules");
492
+ function timeSince(date) {
493
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
494
+ if (seconds < 60) return "just now";
495
+ const minutes = Math.floor(seconds / 60);
496
+ if (minutes < 60) return `${minutes}m ago`;
497
+ const hours = Math.floor(minutes / 60);
498
+ if (hours < 24) return `${hours}h ago`;
499
+ const days = Math.floor(hours / 24);
500
+ return `${days}d ago`;
501
+ }
502
+ __name(timeSince, "timeSince");
503
+ function capitalizeFirst(s) {
504
+ return s.charAt(0).toUpperCase() + s.slice(1);
505
+ }
506
+ __name(capitalizeFirst, "capitalizeFirst");
507
+ async function saveConfig(repoPath, modules) {
508
+ const { select } = await import("@inquirer/prompts");
509
+ const envEmbedding = process.env.BRAINBANK_EMBEDDING;
510
+ const embedding = await select({
511
+ message: "Embedding provider:",
512
+ choices: [
513
+ {
514
+ name: "perplexity-context \u2014 best accuracy (recommended)",
515
+ value: "perplexity-context"
516
+ },
517
+ {
518
+ name: "perplexity \u2014 fast, high quality",
519
+ value: "perplexity"
520
+ },
521
+ {
522
+ name: "openai \u2014 text-embedding-3-small",
523
+ value: "openai"
524
+ },
525
+ {
526
+ name: "local \u2014 offline, no API key needed",
527
+ value: "local"
528
+ }
529
+ ],
530
+ default: envEmbedding ?? "perplexity-context"
531
+ });
532
+ const pruner = await select({
533
+ message: "Noise pruner:",
534
+ choices: [
535
+ {
536
+ name: "none \u2014 no pruning (default)",
537
+ value: "none"
538
+ },
539
+ {
540
+ name: "haiku \u2014 AI-powered noise filter (Claude Haiku)",
541
+ value: "haiku"
542
+ }
543
+ ],
544
+ default: "none"
545
+ });
546
+ const configDir = path2.join(repoPath, ".brainbank");
547
+ const configPath = path2.join(configDir, "config.json");
548
+ const config = {
549
+ plugins: modules,
550
+ embedding
551
+ };
552
+ if (pruner !== "none") {
553
+ config.pruner = pruner;
554
+ }
555
+ fs2.mkdirSync(configDir, { recursive: true });
556
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
557
+ console.log(c.green(` \u2713 Saved ${path2.relative(process.cwd(), configPath)}`));
558
+ }
559
+ __name(saveConfig, "saveConfig");
352
560
 
353
561
  // src/cli/commands/collection.ts
354
562
  async function cmdCollection() {
@@ -365,7 +573,12 @@ async function cmdCollection() {
365
573
  process.exit(1);
366
574
  }
367
575
  const brain = await createBrain();
368
- await brain.addCollection({
576
+ const docsPlugin = findDocsPlugin(brain);
577
+ if (!docsPlugin) {
578
+ console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
579
+ process.exit(1);
580
+ }
581
+ await docsPlugin.addCollection({
369
582
  name,
370
583
  path: path3,
371
584
  pattern,
@@ -380,7 +593,13 @@ async function cmdCollection() {
380
593
  if (sub === "list") {
381
594
  const brain = await createBrain();
382
595
  await brain.initialize();
383
- const collections = brain.listCollections();
596
+ const docsPlugin = findDocsPlugin(brain);
597
+ if (!docsPlugin) {
598
+ console.log(c.yellow(" Docs plugin not loaded."));
599
+ brain.close();
600
+ return;
601
+ }
602
+ const collections = docsPlugin.listCollections();
384
603
  if (collections.length === 0) {
385
604
  console.log(c.yellow(" No collections registered."));
386
605
  } else {
@@ -401,7 +620,12 @@ async function cmdCollection() {
401
620
  process.exit(1);
402
621
  }
403
622
  const brain = await createBrain();
404
- await brain.removeCollection(name);
623
+ const docsPlugin = findDocsPlugin(brain);
624
+ if (!docsPlugin) {
625
+ console.log(c.red("Docs plugin not loaded."));
626
+ process.exit(1);
627
+ }
628
+ await docsPlugin.removeCollection(name);
405
629
  console.log(c.green(`\u2713 Collection '${name}' removed.`));
406
630
  brain.close();
407
631
  return;
@@ -544,10 +768,16 @@ async function cmdDocs() {
544
768
  opts.onProgress = (col, file, cur, total) => {
545
769
  process.stdout.write(`\r ${c.cyan(col)} [${cur}/${total}] ${file} `);
546
770
  };
547
- const results = await brain.indexDocs(opts);
771
+ const docsPlugin = findDocsPlugin(brain);
772
+ if (!docsPlugin) {
773
+ console.log(c.red(" Docs plugin not loaded. Install @brainbank/docs."));
774
+ process.exit(1);
775
+ }
776
+ const results = await docsPlugin.indexDocs(opts);
548
777
  console.log("\n");
549
778
  for (const [name, stat] of Object.entries(results)) {
550
- console.log(` ${c.green(name)}: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
779
+ const removedStr = stat.removed > 0 ? `, ${c.red(String(stat.removed) + " removed")}` : "";
780
+ console.log(` ${c.green(name)}: ${stat.indexed} indexed, ${stat.skipped} skipped${removedStr}, ${stat.chunks} chunks`);
551
781
  }
552
782
  brain.close();
553
783
  }
@@ -564,7 +794,12 @@ async function cmdDocSearch() {
564
794
  console.log(c.bold(`
565
795
  \u2501\u2501\u2501 BrainBank Doc Search: "${query}" \u2501\u2501\u2501
566
796
  `));
567
- const results = await brain.searchDocs(query, { collection: collection ?? void 0, k });
797
+ const docsPlugin = findDocsPlugin(brain);
798
+ if (!docsPlugin) {
799
+ console.log(c.red("Docs plugin not loaded. Install @brainbank/docs."));
800
+ process.exit(1);
801
+ }
802
+ const results = await docsPlugin.search(query, { collection: collection ?? void 0, k });
568
803
  if (results.length === 0) {
569
804
  console.log(c.yellow(" No results found."));
570
805
  brain.close();
@@ -573,7 +808,7 @@ async function cmdDocSearch() {
573
808
  for (const r of results) {
574
809
  const score = Math.round(r.score * 100);
575
810
  const ctx = r.context ? ` \u2014 ${c.dim(r.context)}` : "";
576
- console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.metadata.collection}]${ctx}`);
811
+ console.log(`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath)} [${r.type === "document" ? r.metadata.collection ?? "" : ""}]${ctx}`);
577
812
  const preview = r.content.split("\n").slice(0, 4).join("\n");
578
813
  console.log(c.dim(preview));
579
814
  console.log("");
@@ -583,56 +818,244 @@ async function cmdDocSearch() {
583
818
  __name(cmdDocSearch, "cmdDocSearch");
584
819
 
585
820
  // src/cli/commands/search.ts
821
+ function parseSourceFlags() {
822
+ const NON_SOURCE_FLAGS = /* @__PURE__ */ new Set([
823
+ "repo",
824
+ "depth",
825
+ "collection",
826
+ "pattern",
827
+ "context",
828
+ "name",
829
+ "keep",
830
+ "reranker",
831
+ "pruner",
832
+ "only",
833
+ "docs-path",
834
+ "mode",
835
+ "limit",
836
+ "ignore",
837
+ "meta",
838
+ "k",
839
+ "yes",
840
+ "y",
841
+ "force",
842
+ "verbose"
843
+ ]);
844
+ const sources = {};
845
+ const positional = [];
846
+ for (let i = 0; i < args.length; i++) {
847
+ if (args[i].startsWith("--")) {
848
+ const name = args[i].slice(2);
849
+ if (name === "yes" || name === "force" || name === "verbose") continue;
850
+ const next = args[i + 1];
851
+ if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE_FLAGS.has(name)) {
852
+ sources[name] = parseInt(next, 10);
853
+ i++;
854
+ continue;
855
+ }
856
+ if (NON_SOURCE_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
857
+ i++;
858
+ }
859
+ continue;
860
+ }
861
+ positional.push(args[i]);
862
+ }
863
+ const query = positional.slice(1).join(" ");
864
+ return { sources, query };
865
+ }
866
+ __name(parseSourceFlags, "parseSourceFlags");
867
+ function printFilterInfo(sources) {
868
+ const entries = Object.entries(sources);
869
+ if (entries.length === 0) return;
870
+ const parts = entries.map(([k, v]) => `${k}=${v}`);
871
+ console.log(c.dim(` Sources: ${parts.join(", ")}`));
872
+ }
873
+ __name(printFilterInfo, "printFilterInfo");
874
+ function buildSearchOptions(sources) {
875
+ return Object.keys(sources).length > 0 ? { sources, source: "cli" } : { sources: {}, source: "cli" };
876
+ }
877
+ __name(buildSearchOptions, "buildSearchOptions");
586
878
  async function cmdSearch() {
587
- const query = stripFlags(args).slice(1).join(" ");
879
+ const { sources, query } = parseSourceFlags();
588
880
  if (!query) {
589
- console.log(c.red("Usage: brainbank search <query>"));
881
+ console.log(c.red("Usage: brainbank search <query> [--code <n>] [--git <n>] [--<source> <n>]"));
590
882
  process.exit(1);
591
883
  }
592
884
  const brain = await createBrain();
593
885
  console.log(c.bold(`
594
886
  \u2501\u2501\u2501 BrainBank Search: "${query}" \u2501\u2501\u2501
595
887
  `));
596
- const results = await brain.search(query);
888
+ printFilterInfo(sources);
889
+ const opts = buildSearchOptions(sources);
890
+ const results = await brain.search(query, opts);
597
891
  printResults(results);
598
892
  brain.close();
599
893
  }
600
894
  __name(cmdSearch, "cmdSearch");
601
895
  async function cmdHybridSearch() {
602
- const query = stripFlags(args).slice(1).join(" ");
896
+ const { sources, query } = parseSourceFlags();
603
897
  if (!query) {
604
- console.log(c.red("Usage: brainbank hsearch <query>"));
898
+ console.log(c.red("Usage: brainbank hsearch <query> [--code <n>] [--git <n>] [--docs <n>] [--<source> <n>]"));
605
899
  process.exit(1);
606
900
  }
607
901
  const brain = await createBrain();
608
902
  console.log(c.bold(`
609
903
  \u2501\u2501\u2501 BrainBank Hybrid Search: "${query}" \u2501\u2501\u2501`));
610
- console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion
611
- `));
612
- const results = await brain.hybridSearch(query);
904
+ console.log(c.dim(` Mode: vector + BM25 \u2192 Reciprocal Rank Fusion`));
905
+ printFilterInfo(sources);
906
+ console.log("");
907
+ const opts = buildSearchOptions(sources);
908
+ const results = await brain.hybridSearch(query, opts);
613
909
  printResults(results);
614
910
  brain.close();
615
911
  }
616
912
  __name(cmdHybridSearch, "cmdHybridSearch");
617
913
  async function cmdKeywordSearch() {
618
- const query = stripFlags(args).slice(1).join(" ");
914
+ const { sources, query } = parseSourceFlags();
619
915
  if (!query) {
620
- console.log(c.red("Usage: brainbank ksearch <query>"));
916
+ console.log(c.red("Usage: brainbank ksearch <query> [--code <n>] [--git <n>] [--<source> <n>]"));
621
917
  process.exit(1);
622
918
  }
623
919
  const brain = await createBrain();
624
920
  await brain.initialize();
625
921
  console.log(c.bold(`
626
922
  \u2501\u2501\u2501 BrainBank Keyword Search: "${query}" \u2501\u2501\u2501`));
627
- console.log(c.dim(` Mode: BM25 full-text (instant)
628
- `));
629
- const results = await brain.searchBM25(query);
923
+ console.log(c.dim(` Mode: BM25 full-text (instant)`));
924
+ printFilterInfo(sources);
925
+ console.log("");
926
+ const opts = buildSearchOptions(sources);
927
+ const results = await brain.searchBM25(query, opts);
630
928
  printResults(results);
631
929
  brain.close();
632
930
  }
633
931
  __name(cmdKeywordSearch, "cmdKeywordSearch");
634
932
 
933
+ // src/cli/server-client.ts
934
+ import * as http from "http";
935
+ async function tryServerContext(options) {
936
+ const info = isServerRunning();
937
+ if (!info) return null;
938
+ try {
939
+ const body = JSON.stringify({
940
+ task: options.task,
941
+ repo: options.repo,
942
+ sources: options.sources,
943
+ pathPrefix: options.pathPrefix,
944
+ affectedFiles: options.affectedFiles
945
+ });
946
+ const response = await httpPost(info.port, "/context", body);
947
+ const data = JSON.parse(response);
948
+ if (data.error) return null;
949
+ return data.context ?? null;
950
+ } catch {
951
+ return null;
952
+ }
953
+ }
954
+ __name(tryServerContext, "tryServerContext");
955
+ async function serverHealth() {
956
+ const info = isServerRunning();
957
+ if (!info) return null;
958
+ try {
959
+ const response = await httpGet(info.port, "/health");
960
+ return JSON.parse(response);
961
+ } catch {
962
+ return null;
963
+ }
964
+ }
965
+ __name(serverHealth, "serverHealth");
966
+ function httpPost(port, path3, body) {
967
+ return new Promise((resolve3, reject) => {
968
+ const req = http.request({
969
+ hostname: "127.0.0.1",
970
+ port,
971
+ path: path3,
972
+ method: "POST",
973
+ headers: {
974
+ "Content-Type": "application/json",
975
+ "Content-Length": Buffer.byteLength(body)
976
+ },
977
+ timeout: 12e4
978
+ // 2 minutes — context queries can be slow on first load
979
+ }, (res) => {
980
+ const chunks = [];
981
+ res.on("data", (chunk) => chunks.push(chunk));
982
+ res.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
983
+ });
984
+ req.on("error", reject);
985
+ req.on("timeout", () => {
986
+ req.destroy();
987
+ reject(new Error("Request timed out"));
988
+ });
989
+ req.write(body);
990
+ req.end();
991
+ });
992
+ }
993
+ __name(httpPost, "httpPost");
994
+ function httpGet(port, path3) {
995
+ return new Promise((resolve3, reject) => {
996
+ const req = http.request({
997
+ hostname: "127.0.0.1",
998
+ port,
999
+ path: path3,
1000
+ method: "GET",
1001
+ timeout: 5e3
1002
+ }, (res) => {
1003
+ const chunks = [];
1004
+ res.on("data", (chunk) => chunks.push(chunk));
1005
+ res.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1006
+ });
1007
+ req.on("error", reject);
1008
+ req.on("timeout", () => {
1009
+ req.destroy();
1010
+ reject(new Error("Request timed out"));
1011
+ });
1012
+ req.end();
1013
+ });
1014
+ }
1015
+ __name(httpGet, "httpGet");
1016
+
635
1017
  // src/cli/commands/context.ts
1018
+ function parseContextFlags() {
1019
+ const NON_SOURCE = /* @__PURE__ */ new Set([
1020
+ "repo",
1021
+ "depth",
1022
+ "collection",
1023
+ "pattern",
1024
+ "context",
1025
+ "name",
1026
+ "keep",
1027
+ "reranker",
1028
+ "pruner",
1029
+ "only",
1030
+ "docs-path",
1031
+ "mode",
1032
+ "limit",
1033
+ "ignore",
1034
+ "meta",
1035
+ "k",
1036
+ "yes",
1037
+ "y",
1038
+ "force",
1039
+ "verbose",
1040
+ "path"
1041
+ ]);
1042
+ const sources = {};
1043
+ for (let i = 0; i < args.length; i++) {
1044
+ if (!args[i].startsWith("--")) continue;
1045
+ const name = args[i].slice(2);
1046
+ if (name.startsWith("no-")) {
1047
+ sources[name.slice(3)] = 0;
1048
+ continue;
1049
+ }
1050
+ const next = args[i + 1];
1051
+ if (next !== void 0 && /^\d+$/.test(next) && !NON_SOURCE.has(name)) {
1052
+ sources[name] = parseInt(next, 10);
1053
+ i++;
1054
+ }
1055
+ }
1056
+ return sources;
1057
+ }
1058
+ __name(parseContextFlags, "parseContextFlags");
636
1059
  async function cmdContext() {
637
1060
  const pos = stripFlags(args);
638
1061
  const sub = pos[1];
@@ -646,7 +1069,12 @@ async function cmdContext() {
646
1069
  }
647
1070
  const brain2 = await createBrain();
648
1071
  await brain2.initialize();
649
- brain2.addContext(collection, path3, desc);
1072
+ const docsPlugin = findDocsPlugin(brain2);
1073
+ if (!docsPlugin) {
1074
+ console.log(c.red("Docs plugin not loaded."));
1075
+ process.exit(1);
1076
+ }
1077
+ docsPlugin.addContext(collection, path3, desc);
650
1078
  console.log(c.green(`\u2713 Context added: ${collection}:${path3} \u2192 "${desc}"`));
651
1079
  brain2.close();
652
1080
  return;
@@ -654,7 +1082,13 @@ async function cmdContext() {
654
1082
  if (sub === "list") {
655
1083
  const brain2 = await createBrain();
656
1084
  await brain2.initialize();
657
- const contexts = brain2.listContexts();
1085
+ const docsPlugin = findDocsPlugin(brain2);
1086
+ if (!docsPlugin) {
1087
+ console.log(c.yellow(" Docs plugin not loaded."));
1088
+ brain2.close();
1089
+ return;
1090
+ }
1091
+ const contexts = docsPlugin.listContexts();
658
1092
  if (contexts.length === 0) {
659
1093
  console.log(c.yellow(" No contexts configured."));
660
1094
  } else {
@@ -673,14 +1107,126 @@ async function cmdContext() {
673
1107
  console.log(c.dim(" brainbank context list"));
674
1108
  process.exit(1);
675
1109
  }
1110
+ const sources = parseContextFlags();
1111
+ const pathPrefix = getFlag("path");
1112
+ const repo = getFlag("repo");
1113
+ const fields = parseFieldFlags();
1114
+ const serverResult = await tryServerContext({
1115
+ task,
1116
+ repo: repo ?? process.cwd(),
1117
+ sources: Object.keys(sources).length > 0 ? sources : void 0,
1118
+ pathPrefix
1119
+ });
1120
+ if (serverResult !== null) {
1121
+ console.log(serverResult);
1122
+ return;
1123
+ }
676
1124
  const brain = await createBrain();
677
- const context = await brain.getContext(task);
1125
+ const context = await brain.getContext(task, {
1126
+ sources: Object.keys(sources).length > 0 ? sources : void 0,
1127
+ pathPrefix,
1128
+ source: "cli",
1129
+ fields: Object.keys(fields).length > 0 ? fields : void 0
1130
+ });
678
1131
  console.log(context);
679
1132
  brain.close();
680
1133
  }
681
1134
  __name(cmdContext, "cmdContext");
1135
+ function parseFieldFlags() {
1136
+ const FIELD_BOOLEANS = /* @__PURE__ */ new Set(["lines", "symbols", "compact", "expander"]);
1137
+ const FIELD_NEGATABLE = /* @__PURE__ */ new Set(["callTree", "imports"]);
1138
+ const fields = {};
1139
+ for (let i = 0; i < args.length; i++) {
1140
+ if (!args[i].startsWith("--")) continue;
1141
+ const raw = args[i].slice(2);
1142
+ if (raw.startsWith("no-")) {
1143
+ const name = raw.slice(3);
1144
+ if (FIELD_NEGATABLE.has(name)) {
1145
+ fields[name] = false;
1146
+ }
1147
+ continue;
1148
+ }
1149
+ const dotIdx = raw.indexOf(".");
1150
+ if (dotIdx > 0) {
1151
+ const fieldName = raw.slice(0, dotIdx);
1152
+ const rest = raw.slice(dotIdx + 1);
1153
+ const eqIdx = rest.indexOf("=");
1154
+ if (eqIdx > 0) {
1155
+ const key = rest.slice(0, eqIdx);
1156
+ const val = parseInt(rest.slice(eqIdx + 1), 10);
1157
+ if (!isNaN(val)) {
1158
+ fields[fieldName] = { [key]: val };
1159
+ }
1160
+ }
1161
+ continue;
1162
+ }
1163
+ if (FIELD_BOOLEANS.has(raw)) {
1164
+ fields[raw] = true;
1165
+ }
1166
+ }
1167
+ return fields;
1168
+ }
1169
+ __name(parseFieldFlags, "parseFieldFlags");
1170
+
1171
+ // src/cli/commands/files.ts
1172
+ async function cmdFiles() {
1173
+ const patterns = [];
1174
+ const showLines = args.includes("--lines");
1175
+ for (let i = 1; i < args.length; i++) {
1176
+ if (args[i].startsWith("--")) {
1177
+ if (args[i] === "--repo") {
1178
+ i++;
1179
+ }
1180
+ continue;
1181
+ }
1182
+ patterns.push(args[i]);
1183
+ }
1184
+ if (patterns.length === 0) {
1185
+ console.log(c.red("Usage: brainbank files <path|glob> [...paths] [--lines]"));
1186
+ console.log(c.dim(" Exact: brainbank files src/auth/login.ts"));
1187
+ console.log(c.dim(" Directory: brainbank files src/graph/"));
1188
+ console.log(c.dim(' Glob: brainbank files "src/**/*.service.ts"'));
1189
+ console.log(c.dim(" Fuzzy: brainbank files plugin.ts"));
1190
+ console.log(c.dim(" Lines: brainbank files src/plugin.ts --lines"));
1191
+ process.exit(1);
1192
+ }
1193
+ const brain = await createBrain();
1194
+ await brain.initialize();
1195
+ const results = brain.resolveFiles(patterns);
1196
+ if (results.length === 0) {
1197
+ console.log(c.yellow("No matching files found in the index."));
1198
+ console.log(c.dim("Run `brainbank index` first to index your codebase."));
1199
+ brain.close();
1200
+ return;
1201
+ }
1202
+ for (const r of results) {
1203
+ const meta = r.metadata;
1204
+ const startLine = meta.startLine ?? 1;
1205
+ console.log(c.bold(`
1206
+ \u2500\u2500 ${r.filePath} \u2500\u2500
1207
+ `));
1208
+ if (showLines) {
1209
+ const codeLines = r.content.split("\n");
1210
+ const pad = String(startLine + codeLines.length - 1).length;
1211
+ for (let i = 0; i < codeLines.length; i++) {
1212
+ const lineNum = c.dim(`${String(startLine + i).padStart(pad)}|`);
1213
+ console.log(`${lineNum} ${codeLines[i]}`);
1214
+ }
1215
+ } else {
1216
+ console.log(r.content);
1217
+ }
1218
+ }
1219
+ console.log(c.dim(`
1220
+ ${results.length} file(s) resolved.`));
1221
+ brain.close();
1222
+ }
1223
+ __name(cmdFiles, "cmdFiles");
682
1224
 
683
- // src/cli/commands/system.ts
1225
+ // src/cli/commands/stats.ts
1226
+ function formatStatKey(key) {
1227
+ return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase()).padEnd(16);
1228
+ }
1229
+ __name(formatStatKey, "formatStatKey");
684
1230
  async function cmdStats() {
685
1231
  const brain = await createBrain();
686
1232
  await brain.initialize();
@@ -688,27 +1234,12 @@ async function cmdStats() {
688
1234
  console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Stats \u2501\u2501\u2501\n"));
689
1235
  console.log(` ${c.cyan("Plugins")}: ${brain.plugins.join(", ")}
690
1236
  `);
691
- if (s.code) {
692
- console.log(` ${c.cyan("Code")}`);
693
- console.log(` Files indexed: ${s.code.files}`);
694
- console.log(` Code chunks: ${s.code.chunks}`);
695
- console.log(` HNSW vectors: ${s.code.hnswSize}`);
696
- console.log("");
697
- }
698
- if (s.git) {
699
- console.log(` ${c.cyan("Git History")}`);
700
- console.log(` Commits: ${s.git.commits}`);
701
- console.log(` Files tracked: ${s.git.filesTracked}`);
702
- console.log(` Co-edit pairs: ${s.git.coEdits}`);
703
- console.log(` HNSW vectors: ${s.git.hnswSize}`);
704
- console.log("");
705
- }
706
- if (s.documents) {
707
- console.log(` ${c.cyan("Documents")}`);
708
- console.log(` Collections: ${s.documents.collections}`);
709
- console.log(` Documents: ${s.documents.documents}`);
710
- console.log(` Chunks: ${s.documents.chunks}`);
711
- console.log(` HNSW vectors: ${s.documents.hnswSize}`);
1237
+ for (const [name, pluginStats] of Object.entries(s)) {
1238
+ if (!pluginStats) continue;
1239
+ console.log(` ${c.cyan(name)}`);
1240
+ for (const [key, value] of Object.entries(pluginStats)) {
1241
+ console.log(` ${formatStatKey(key)}${value}`);
1242
+ }
712
1243
  console.log("");
713
1244
  }
714
1245
  const kvNames = brain.listCollectionNames();
@@ -723,6 +1254,8 @@ async function cmdStats() {
723
1254
  brain.close();
724
1255
  }
725
1256
  __name(cmdStats, "cmdStats");
1257
+
1258
+ // src/cli/commands/reembed.ts
726
1259
  async function cmdReembed() {
727
1260
  const brain = await createBrain();
728
1261
  await brain.initialize();
@@ -735,29 +1268,37 @@ async function cmdReembed() {
735
1268
  }, "onProgress")
736
1269
  });
737
1270
  console.log("\n");
738
- if (result.code > 0) console.log(` ${c.green("\u2713")} Code: ${result.code} vectors`);
739
- if (result.git > 0) console.log(` ${c.green("\u2713")} Git: ${result.git} vectors`);
740
- if (result.docs > 0) console.log(` ${c.green("\u2713")} Docs: ${result.docs} vectors`);
741
- if (result.kv > 0) console.log(` ${c.green("\u2713")} KV: ${result.kv} vectors`);
742
- if (result.notes > 0) console.log(` ${c.green("\u2713")} Notes: ${result.notes} vectors`);
743
- if (result.memory > 0) console.log(` ${c.green("\u2713")} Memory: ${result.memory} vectors`);
1271
+ for (const [name, count] of Object.entries(result.counts)) {
1272
+ if (count > 0) {
1273
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
1274
+ console.log(` ${c.green("\u2713")} ${label.padEnd(8)} ${count} vectors`);
1275
+ }
1276
+ }
744
1277
  console.log(`
745
1278
  ${c.bold("Total")}: ${result.total} vectors regenerated
746
1279
  `);
747
1280
  brain.close();
748
1281
  }
749
1282
  __name(cmdReembed, "cmdReembed");
1283
+
1284
+ // src/cli/commands/watch.ts
750
1285
  async function cmdWatch() {
751
1286
  const brain = await createBrain();
752
1287
  await brain.initialize();
1288
+ const config = await loadConfig(brain.config.repoPath);
1289
+ const codeIgnore = config?.code?.ignore ?? [];
753
1290
  console.log(c.bold("\n\u2501\u2501\u2501 BrainBank Watch \u2501\u2501\u2501\n"));
754
1291
  console.log(c.dim(` Watching ${brain.config.repoPath} for changes...`));
1292
+ if (codeIgnore.length > 0) {
1293
+ console.log(c.dim(` Ignoring: ${codeIgnore.join(", ")}`));
1294
+ }
755
1295
  console.log(c.dim(" Press Ctrl+C to stop.\n"));
756
1296
  const watcher = brain.watch({
757
1297
  debounceMs: 2e3,
758
- onIndex: /* @__PURE__ */ __name((file, indexer) => {
1298
+ ignore: codeIgnore,
1299
+ onIndex: /* @__PURE__ */ __name((sourceId, pluginName) => {
759
1300
  const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
760
- console.log(` ${c.dim(ts)} ${c.green("\u2713")} ${c.cyan(indexer)}: ${file}`);
1301
+ console.log(` ${c.dim(ts)} ${c.green("\u2713")} ${c.cyan(pluginName)}: ${sourceId}`);
761
1302
  }, "onIndex"),
762
1303
  onError: /* @__PURE__ */ __name((err) => {
763
1304
  console.error(` ${c.red("\u2717")} ${err.message}`);
@@ -773,14 +1314,141 @@ async function cmdWatch() {
773
1314
  });
774
1315
  }
775
1316
  __name(cmdWatch, "cmdWatch");
776
- async function cmdServe() {
777
- await import("@brainbank/mcp");
1317
+
1318
+ // src/cli/commands/mcp.ts
1319
+ async function cmdMcp() {
1320
+ try {
1321
+ await import("@brainbank/mcp");
1322
+ } catch {
1323
+ console.error(c.red("Error: @brainbank/mcp is not installed."));
1324
+ console.error(c.dim(" Install: npm i @brainbank/mcp"));
1325
+ process.exit(1);
1326
+ }
778
1327
  }
779
- __name(cmdServe, "cmdServe");
1328
+ __name(cmdMcp, "cmdMcp");
1329
+
1330
+ // src/cli/commands/daemon.ts
1331
+ async function cmdDaemon() {
1332
+ const pos = stripFlags(args);
1333
+ const sub = pos[1];
1334
+ if (sub === "stop") return stopDaemon();
1335
+ if (sub === "restart") {
1336
+ stopDaemon();
1337
+ return forkDaemon();
1338
+ }
1339
+ if (sub === "start") return forkDaemon();
1340
+ return startForeground();
1341
+ }
1342
+ __name(cmdDaemon, "cmdDaemon");
1343
+ async function startForeground() {
1344
+ const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
1345
+ const { HttpServer } = await import("./http-server-GIRELCCL.js");
1346
+ const server = new HttpServer({
1347
+ port,
1348
+ factory: /* @__PURE__ */ __name(async (repoPath) => {
1349
+ const brain = await createBrain(repoPath);
1350
+ await brain.initialize();
1351
+ return brain;
1352
+ }, "factory"),
1353
+ onError: /* @__PURE__ */ __name((repo, err) => {
1354
+ const msg = err instanceof Error ? err.message : String(err);
1355
+ console.error(c.red(` Pool error [${repo}]: ${msg}`));
1356
+ }, "onError"),
1357
+ onLog: /* @__PURE__ */ __name((msg) => console.log(c.dim(` ${msg}`)), "onLog")
1358
+ });
1359
+ console.log(c.bold("\n\u2501\u2501\u2501 BrainBank HTTP Daemon \u2501\u2501\u2501\n"));
1360
+ await server.start();
1361
+ console.log(c.dim(` Port: ${port}`));
1362
+ console.log(c.dim(" Press Ctrl+C to stop.\n"));
1363
+ const shutdown = /* @__PURE__ */ __name(() => {
1364
+ console.log(c.dim("\n Shutting down..."));
1365
+ server.close();
1366
+ process.exit(0);
1367
+ }, "shutdown");
1368
+ process.on("SIGINT", shutdown);
1369
+ process.on("SIGTERM", shutdown);
1370
+ await new Promise(() => {
1371
+ });
1372
+ }
1373
+ __name(startForeground, "startForeground");
1374
+ async function forkDaemon() {
1375
+ const port = parseInt(getFlag("port") ?? String(DEFAULT_PORT), 10);
1376
+ const { fork } = await import("child_process");
1377
+ const existing = isServerRunning();
1378
+ if (existing) {
1379
+ console.log(c.yellow(` Daemon already running (PID ${existing.pid}, port ${existing.port})`));
1380
+ return;
1381
+ }
1382
+ const child = fork(process.argv[1], ["daemon", "--port", String(port)], {
1383
+ detached: true,
1384
+ stdio: "ignore"
1385
+ });
1386
+ child.unref();
1387
+ console.log(c.green(` \u2713 Daemon started (PID ${child.pid}, port ${port})`));
1388
+ console.log(c.dim(" Stop with: brainbank daemon stop"));
1389
+ }
1390
+ __name(forkDaemon, "forkDaemon");
1391
+ function stopDaemon() {
1392
+ const info = isServerRunning();
1393
+ if (!info) {
1394
+ console.log(c.yellow(" No daemon running."));
1395
+ return;
1396
+ }
1397
+ try {
1398
+ process.kill(info.pid, "SIGTERM");
1399
+ removePid();
1400
+ console.log(c.green(` \u2713 Daemon stopped (PID ${info.pid})`));
1401
+ } catch {
1402
+ removePid();
1403
+ console.log(c.yellow(` PID ${info.pid} not found. Cleaned up stale PID file.`));
1404
+ }
1405
+ }
1406
+ __name(stopDaemon, "stopDaemon");
1407
+
1408
+ // src/cli/commands/status.ts
1409
+ function formatUptime(seconds) {
1410
+ if (seconds < 60) return `${seconds}s`;
1411
+ const minutes = Math.floor(seconds / 60);
1412
+ if (minutes < 60) return `${minutes}m`;
1413
+ const hours = Math.floor(minutes / 60);
1414
+ const remainMinutes = minutes % 60;
1415
+ return remainMinutes > 0 ? `${hours}h ${remainMinutes}m` : `${hours}h`;
1416
+ }
1417
+ __name(formatUptime, "formatUptime");
1418
+ async function cmdStatus() {
1419
+ const info = isServerRunning();
1420
+ if (!info) {
1421
+ console.log(`
1422
+ ${c.dim("HTTP Server:")} ${c.yellow("stopped")}
1423
+ `);
1424
+ console.log(c.dim(" Start with: brainbank daemon"));
1425
+ console.log("");
1426
+ return;
1427
+ }
1428
+ const health = await serverHealth();
1429
+ if (health) {
1430
+ const uptime = formatUptime(health.uptime);
1431
+ console.log(`
1432
+ ${c.dim("HTTP Server:")} ${c.green("running")}`);
1433
+ console.log(` ${c.dim("PID:")} ${health.pid}`);
1434
+ console.log(` ${c.dim("Port:")} ${health.port}`);
1435
+ console.log(` ${c.dim("Uptime:")} ${uptime}`);
1436
+ console.log(` ${c.dim("Workspaces:")} ${health.workspaces}`);
1437
+ console.log("");
1438
+ } else {
1439
+ console.log(`
1440
+ ${c.dim("HTTP Server:")} ${c.yellow("stale")} (PID ${info.pid} not responding)`);
1441
+ console.log(c.dim(" The PID file may be stale. Restart with: brainbank daemon"));
1442
+ console.log("");
1443
+ }
1444
+ }
1445
+ __name(cmdStatus, "cmdStatus");
1446
+
1447
+ // src/cli/commands/help.ts
780
1448
  function showHelp() {
781
1449
  console.log(c.bold("\n\u2501\u2501\u2501 BrainBank \u2014 Semantic Knowledge Bank \u2501\u2501\u2501\n"));
782
1450
  console.log(c.bold("Indexing:"));
783
- console.log(` ${c.cyan("index")} [path] Index code + git history`);
1451
+ console.log(` ${c.cyan("index")} ${c.dim("(i)")} [path] Index code + git history`);
784
1452
  console.log(` ${c.cyan("collection add")} <path> --name Add a document collection`);
785
1453
  console.log(` ${c.cyan("collection list")} List collections`);
786
1454
  console.log(` ${c.cyan("collection remove")} <name> Remove a collection`);
@@ -791,11 +1459,13 @@ function showHelp() {
791
1459
  console.log(` ${c.cyan("hsearch")} <query> Hybrid search (${c.bold("best quality")})`);
792
1460
  console.log(` ${c.cyan("ksearch")} <query> Keyword search (BM25, instant)`);
793
1461
  console.log(` ${c.cyan("dsearch")} <query> Document search`);
1462
+ console.log(c.dim(" All search commands accept --<source> <n> to filter by source"));
794
1463
  console.log("");
795
1464
  console.log(c.bold("Context:"));
796
1465
  console.log(` ${c.cyan("context")} <task> Get formatted context for a task`);
797
1466
  console.log(` ${c.cyan("context add")} <col> <path> <desc> Add context metadata`);
798
1467
  console.log(` ${c.cyan("context list")} List all context metadata`);
1468
+ console.log(` ${c.cyan("files")} <path|glob> [...] [--lines] View full indexed files directly`);
799
1469
  console.log("");
800
1470
  console.log(c.bold("KV Store:"));
801
1471
  console.log(` ${c.cyan("kv add")} <coll> <content> Add item to a collection`);
@@ -808,7 +1478,12 @@ function showHelp() {
808
1478
  console.log(` ${c.cyan("stats")} Show index statistics`);
809
1479
  console.log(` ${c.cyan("reembed")} Re-embed all vectors`);
810
1480
  console.log(` ${c.cyan("watch")} Watch files, auto-re-index`);
811
- console.log(` ${c.cyan("serve")} Start MCP server (stdio)`);
1481
+ console.log(` ${c.cyan("mcp")} Start MCP server (stdio)`);
1482
+ console.log(` ${c.cyan("daemon")} Start HTTP daemon (foreground)`);
1483
+ console.log(` ${c.cyan("daemon start")} Start HTTP daemon (background)`);
1484
+ console.log(` ${c.cyan("daemon stop")} Stop background daemon`);
1485
+ console.log(` ${c.cyan("daemon restart")} Restart background daemon`);
1486
+ console.log(` ${c.cyan("status")} Show daemon status`);
812
1487
  console.log("");
813
1488
  console.log(c.bold("Options:"));
814
1489
  console.log(` ${c.dim("--repo <path>")} Repository path (default: .)`);
@@ -817,16 +1492,26 @@ function showHelp() {
817
1492
  console.log(` ${c.dim("--collection <name>")} Filter by collection`);
818
1493
  console.log(` ${c.dim("--pattern <glob>")} Collection glob (default: **/*.md)`);
819
1494
  console.log(` ${c.dim("--context <desc>")} Context description`);
1495
+ console.log(` ${c.dim("--<source> <n>")} Source filter: max results from <source> (0 = skip)`);
1496
+ console.log(` ${c.dim("--path <dir>")} Filter context results to files under this path prefix`);
1497
+ console.log(` ${c.dim("--ignore <globs>")} Ignore glob patterns for code indexing (comma-separated)`);
1498
+ console.log(` ${c.dim("--yes / -y")} Skip interactive prompt (auto-select all available)`);
820
1499
  console.log(` ${c.dim("--reranker <name>")} Reranker to use (qwen3)`);
1500
+ console.log(` ${c.dim("--port <n>")} HTTP daemon port (default: 8181)`);
821
1501
  console.log("");
822
1502
  console.log(c.bold("Examples:"));
823
1503
  console.log(c.dim(" brainbank index ."));
1504
+ console.log(c.dim(' brainbank index . --ignore "sdk/**,vendor/**"'));
824
1505
  console.log(c.dim(' brainbank kv add errors "Fixed null pointer in api.ts"'));
825
1506
  console.log(c.dim(' brainbank kv search errors "null pointer"'));
826
1507
  console.log(c.dim(" brainbank kv list"));
827
1508
  console.log(c.dim(' brainbank hsearch "authentication middleware"'));
828
- console.log(c.dim(' brainbank context "add rate limiting to the API"'));
829
- console.log(c.dim(" brainbank serve"));
1509
+ console.log(c.dim(' brainbank hsearch "auth" --code 0 --git 10 # git only'));
1510
+ console.log(c.dim(' brainbank search "handler" --git 0 # code only'));
1511
+ console.log(c.dim(' brainbank hsearch "api" --docs 10 --code 0 --git 0 # docs only'));
1512
+ console.log(c.dim(' brainbank context "auth flow" | pbcopy # \u2192 clipboard'));
1513
+ console.log(c.dim(" brainbank daemon start # background HTTP"));
1514
+ console.log(c.dim(" brainbank mcp # MCP stdio"));
830
1515
  }
831
1516
  __name(showHelp, "showHelp");
832
1517
 
@@ -834,6 +1519,7 @@ __name(showHelp, "showHelp");
834
1519
  var command = args[0];
835
1520
  async function main() {
836
1521
  switch (command) {
1522
+ case "i":
837
1523
  case "index":
838
1524
  return cmdIndex();
839
1525
  case "collection":
@@ -852,14 +1538,23 @@ async function main() {
852
1538
  return cmdKeywordSearch();
853
1539
  case "context":
854
1540
  return cmdContext();
1541
+ case "files":
1542
+ return cmdFiles();
855
1543
  case "stats":
856
1544
  return cmdStats();
857
1545
  case "reembed":
858
1546
  return cmdReembed();
859
1547
  case "watch":
860
1548
  return cmdWatch();
1549
+ case "mcp":
1550
+ return cmdMcp();
861
1551
  case "serve":
862
- return cmdServe();
1552
+ return cmdMcp();
1553
+ // backward compat
1554
+ case "daemon":
1555
+ return cmdDaemon();
1556
+ case "status":
1557
+ return cmdStatus();
863
1558
  case "help":
864
1559
  case "--help":
865
1560
  case "-h":