@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,1296 @@
1
+ import {
2
+ BrainTools,
3
+ autoRefreshLine,
4
+ collectStaleFiles,
5
+ formatUnifiedHealth,
6
+ getUnifiedHealth,
7
+ maybeAutoRefresh
8
+ } from "./chunk-22V37YTK.js";
9
+ import {
10
+ IndexLockHeldError,
11
+ ParallelIndexerConfig,
12
+ acquireIndexLock,
13
+ indexProjectParallel
14
+ } from "./chunk-ZETTGPAW.js";
15
+ import {
16
+ getProjectId
17
+ } from "./chunk-DIYAGQJL.js";
18
+ import {
19
+ BACKBENCHER_BRAIN_VERSION
20
+ } from "./chunk-WIG36E67.js";
21
+ import {
22
+ FRESHNESS_REMEDIATION,
23
+ resolveRowsFreshnessDetailed
24
+ } from "./chunk-QLK6ED4Q.js";
25
+ import {
26
+ BrainDatabase
27
+ } from "./chunk-QKTRECYS.js";
28
+ import {
29
+ bold,
30
+ colorEnabled,
31
+ cyan,
32
+ dim,
33
+ green,
34
+ red,
35
+ setLogLevel,
36
+ yellow
37
+ } from "./chunk-BQ3OVQ3P.js";
38
+ import {
39
+ bbProjectsDir
40
+ } from "./chunk-2BSKD7O3.js";
41
+ import {
42
+ describeScopedPathRefusal,
43
+ resolveScopedProjectFile
44
+ } from "./chunk-D2Q4Y47A.js";
45
+
46
+ // src/cli/index.ts
47
+ import { Command } from "commander";
48
+ import { fileURLToPath } from "url";
49
+ import { existsSync, realpathSync, statSync } from "fs";
50
+ import path from "path";
51
+
52
+ // src/cli/index-progress.ts
53
+ var SMALL_PROJECT_FILES = 150;
54
+ var TINY_PROJECT_FILES = 50;
55
+ var ENCOURAGEMENT_MIN_FILES = 50;
56
+ var RATE_WINDOW_MS = 45e3;
57
+ var EXPECTED_MS_PER_FILE = 250;
58
+ var BIG_PROJECT_FILES = 4e3;
59
+ var SLOW_RUN_MS = 3 * 60 * 1e3;
60
+ var SUPPORT_EMAIL = "support@backbencher.cc";
61
+ function encouragementFor(pct, isBigProject2, totalFiles = Number.MAX_SAFE_INTEGER) {
62
+ if (totalFiles < ENCOURAGEMENT_MIN_FILES) return null;
63
+ if (pct >= 100) return null;
64
+ if (pct >= 95) return "almost there \u2014 finishing up";
65
+ if (pct >= 80) return "in the final stretch";
66
+ if (pct >= 50) return "past the halfway mark";
67
+ if (pct >= 25) return "a quarter of the way in";
68
+ return null;
69
+ }
70
+ function isBigProject(totalFiles) {
71
+ return totalFiles >= BIG_PROJECT_FILES;
72
+ }
73
+ function formatDuration(ms) {
74
+ if (ms === null || ms === void 0 || !Number.isFinite(ms) || ms < 0) return "\u2026";
75
+ const total = Math.floor(ms / 1e3);
76
+ const h = Math.floor(total / 3600);
77
+ const m = Math.floor(total % 3600 / 60);
78
+ const s = total % 60;
79
+ if (h > 0) return `${h}h ${m}m ${s}s`;
80
+ if (m > 0) return `${m}m ${s}s`;
81
+ return `${s}s`;
82
+ }
83
+ function expectationFor(totalFiles) {
84
+ if (totalFiles <= 0) return "nothing to index \u2014 the index is already up to date";
85
+ if (totalFiles < TINY_PROJECT_FILES) return "a moment \u2014 this is a tiny project";
86
+ if (totalFiles < SMALL_PROJECT_FILES) return "this is a small project \u2014 a few minutes at most";
87
+ if (totalFiles < BIG_PROJECT_FILES) return "this will take a few minutes";
88
+ return "large project \u2014 this takes a while. Grab a coffee; bb keeps working in the background";
89
+ }
90
+ function countWithCommas(n) {
91
+ if (n === void 0 || n === null || !Number.isFinite(n)) return "?";
92
+ return n.toLocaleString();
93
+ }
94
+ function windowedFilesPerSecond(samples, windowMs = RATE_WINDOW_MS) {
95
+ if (samples.length < 2) return null;
96
+ const newest = samples[samples.length - 1];
97
+ const inWindow = samples.filter((s) => newest.at - s.at <= windowMs);
98
+ const oldest = inWindow.length >= 2 ? inWindow[0] : samples[samples.length - 2];
99
+ const dt = (newest.at - oldest.at) / 1e3;
100
+ const df = newest.files - oldest.files;
101
+ if (dt <= 0.5 || df <= 0) return null;
102
+ return df / dt;
103
+ }
104
+ function estimateRemainingMs(stats, startedAt, samples = [], now = Date.now()) {
105
+ const { processedFiles, totalFiles } = stats;
106
+ if (!totalFiles || processedFiles <= 0) return null;
107
+ const elapsed = now - startedAt;
108
+ if (elapsed < 1e3) return null;
109
+ const remaining = totalFiles - processedFiles;
110
+ if (remaining <= 0) return 0;
111
+ const windowed = windowedFilesPerSecond(samples);
112
+ if (windowed !== null && windowed > 0) {
113
+ const cumulative = processedFiles / (elapsed / 1e3);
114
+ const rate = Math.min(windowed, cumulative * 2);
115
+ if (rate > 0) return remaining / rate * 1e3;
116
+ }
117
+ return elapsed / processedFiles * remaining;
118
+ }
119
+ function formatEta(remainingMs, filesDone) {
120
+ if (remainingMs === null) return "\u2026";
121
+ if (remainingMs <= 0) return filesDone ? "finishing" : "\u2026";
122
+ return formatDuration(remainingMs);
123
+ }
124
+ function expectedDurationMs(totalFiles) {
125
+ return Math.max(SLOW_RUN_MS, totalFiles * EXPECTED_MS_PER_FILE);
126
+ }
127
+ var IndexProgressRenderer = class {
128
+ stream;
129
+ isTTY;
130
+ startedAt;
131
+ totalFiles;
132
+ nonTtyIntervalMs;
133
+ lastRenderAt = 0;
134
+ lastNonTtyBucket = -1;
135
+ lastLineLength = 0;
136
+ announcedExpectation = false;
137
+ lastEncouragementBand = -1;
138
+ /** Recent (timestamp, files-done) observations, for the windowed rate. Bounded. */
139
+ samples = [];
140
+ /** ANSI allowed for this renderer's stream (TTY, minus NO_COLOR; FORCE_COLOR can opt in). */
141
+ color;
142
+ /** Keep only what the window needs, so a long run cannot grow this without bound. */
143
+ recordSample(files) {
144
+ const at = Date.now();
145
+ const last = this.samples[this.samples.length - 1];
146
+ if (last && last.files === files) return;
147
+ this.samples.push({ at, files });
148
+ while (this.samples.length > 2 && at - this.samples[0].at > RATE_WINDOW_MS * 2) this.samples.shift();
149
+ }
150
+ constructor(opts) {
151
+ this.stream = opts.stream;
152
+ this.isTTY = opts.isTTY;
153
+ this.startedAt = opts.startedAt;
154
+ this.totalFiles = opts.totalFiles;
155
+ this.nonTtyIntervalMs = opts.nonTtyIntervalMs ?? 1e4;
156
+ this.color = colorEnabled({ isTTY: opts.isTTY });
157
+ }
158
+ get elapsedMs() {
159
+ return Date.now() - this.startedAt;
160
+ }
161
+ /** One-time framing line once the file count is known. */
162
+ begin() {
163
+ if (this.announcedExpectation) return;
164
+ const expected = expectationFor(this.totalFiles);
165
+ this.announcedExpectation = true;
166
+ this.stream.write(
167
+ `
168
+ ${bold(`Indexing ${countWithCommas(this.totalFiles)} files`, this.color)} \u2014 ${cyan(expected, this.color)}
169
+
170
+ `
171
+ );
172
+ }
173
+ /**
174
+ * Render a progress update. Safe to call very frequently; throttling is
175
+ * internal and differs between TTY and non-TTY.
176
+ */
177
+ update(stats) {
178
+ if (!this.announcedExpectation && stats.totalFiles > 0) {
179
+ this.totalFiles = stats.totalFiles;
180
+ this.begin();
181
+ }
182
+ this.maybeEncourage(stats);
183
+ this.recordSample(stats.processedFiles);
184
+ if (this.isTTY) {
185
+ const now = Date.now();
186
+ if (now - this.lastRenderAt < 80) return;
187
+ this.lastRenderAt = now;
188
+ this.renderTtyLine(stats);
189
+ return;
190
+ }
191
+ this.renderNonTtyLine(stats);
192
+ }
193
+ /**
194
+ * Print a progress milestone at most once per band. Written ABOVE the live bar
195
+ * so it does not fight the redraw for the same line.
196
+ */
197
+ maybeEncourage(stats) {
198
+ const total = stats.totalFiles > 0 ? stats.totalFiles : this.totalFiles;
199
+ if (total <= 0) return;
200
+ const done = stats.processedFiles + (stats.skippedFiles ?? 0);
201
+ const pct = Math.min(100, Math.floor(done / total * 100));
202
+ const line = encouragementFor(pct, isBigProject(total), total);
203
+ if (!line) return;
204
+ const band = pct >= 95 ? 95 : pct >= 80 ? 80 : pct >= 50 ? 50 : pct >= 25 ? 25 : 0;
205
+ if (band === this.lastEncouragementBand) return;
206
+ this.lastEncouragementBand = band;
207
+ if (this.isTTY) {
208
+ this.stream.write(`\r\x1B[2K ${cyan(line, this.color)}
209
+ `);
210
+ this.lastLineLength = 0;
211
+ } else {
212
+ this.stream.write(`${dim("[index]", this.color)} ${cyan(line, this.color)}
213
+ `);
214
+ }
215
+ }
216
+ renderTtyLine(stats) {
217
+ const total = stats.totalFiles > 0 ? stats.totalFiles : this.totalFiles;
218
+ const done = stats.processedFiles + (stats.skippedFiles ?? 0);
219
+ const pct = total > 0 ? Math.min(100, Math.floor(done / total * 100)) : 0;
220
+ const rate = typeof stats.filesPerSecond === "number" ? `${stats.filesPerSecond.toFixed(1)}/s` : "?";
221
+ const remaining = estimateRemainingMs(stats, this.startedAt, this.samples);
222
+ const filesDone = stats.processedFiles >= total;
223
+ const eta = formatEta(remaining, filesDone);
224
+ const elapsed = formatDuration(this.elapsedMs);
225
+ const BAR = 24;
226
+ const filled = total > 0 ? Math.round(pct / 100 * BAR) : 0;
227
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR - filled);
228
+ const parts = [
229
+ ` ${bar} ${bold(String(pct).padStart(3) + "%", this.color)}`,
230
+ `${countWithCommas(done)}/${countWithCommas(total)} files`,
231
+ rate,
232
+ `elapsed ${elapsed}`,
233
+ bold(cyan(`ETA ${eta}`, this.color), this.color)
234
+ ];
235
+ if (stats.symbols !== void 0) parts.push(`${countWithCommas(stats.symbols)} symbols`);
236
+ if (stats.relationships !== void 0) parts.push(`${countWithCommas(stats.relationships)} rels`);
237
+ if (stats.embeddings !== void 0) parts.push(`${countWithCommas(stats.embeddings)} embeddings`);
238
+ let line = parts.join(" ");
239
+ if (stats.errorFiles) line += ` ${yellow(`\u26A0 ${stats.errorFiles} errors`, this.color)}`;
240
+ if (stats.currentFile) {
241
+ const f = stats.currentFile.length > 48 ? "\u2026" + stats.currentFile.slice(-47) : stats.currentFile;
242
+ line += ` ${f}`;
243
+ }
244
+ this.writeTty(line);
245
+ }
246
+ writeTty(line) {
247
+ const pad = this.lastLineLength > line.length ? " ".repeat(this.lastLineLength - line.length) : "";
248
+ this.stream.write(`\r\x1B[2K${line}${pad}`);
249
+ this.lastLineLength = line.length + pad.length;
250
+ }
251
+ /**
252
+ * Non-TTY: emit a milestone line, not a redraw. Bucket by completion percentage
253
+ * AND by wall-clock, so neither a very long run nor a stalled run floods the log.
254
+ */
255
+ renderNonTtyLine(stats) {
256
+ const total = stats.totalFiles > 0 ? stats.totalFiles : this.totalFiles;
257
+ if (total <= 0) return;
258
+ const done = stats.processedFiles + (stats.skippedFiles ?? 0);
259
+ const pct = Math.min(100, Math.floor(done / total * 100));
260
+ const bucket = Math.floor(pct / 5);
261
+ const now = Date.now();
262
+ const dueByBucket = bucket > this.lastNonTtyBucket;
263
+ const dueByTime = now - this.lastRenderAt >= this.nonTtyIntervalMs;
264
+ const isFinal = pct >= 100;
265
+ if (!isFinal && !dueByBucket && !dueByTime) return;
266
+ if (bucket === this.lastNonTtyBucket) return;
267
+ this.lastNonTtyBucket = bucket;
268
+ this.lastRenderAt = now;
269
+ const remaining = estimateRemainingMs(stats, this.startedAt, this.samples);
270
+ const eta = formatEta(remaining, stats.processedFiles >= total);
271
+ this.stream.write(
272
+ `${dim("[index]", this.color)} ${bold(`${pct}%`, this.color)} ${countWithCommas(done)}/${countWithCommas(total)} files elapsed=${formatDuration(this.elapsedMs)} ${cyan(`eta=${eta}`, this.color)}` + (stats.errorFiles ? ` ${yellow(`errors=${stats.errorFiles}`, this.color)}` : "") + `
273
+ `
274
+ );
275
+ }
276
+ /** Clear the live line before the summary prints. */
277
+ clear() {
278
+ if (this.isTTY) {
279
+ this.stream.write("\r\x1B[2K");
280
+ this.lastLineLength = 0;
281
+ }
282
+ }
283
+ /**
284
+ * Post-run messaging. `ok: false` means the run failed — we do not congratulate
285
+ * the user or suggest a support ticket for a run that did not finish.
286
+ */
287
+ finish(result) {
288
+ this.clear();
289
+ const elapsed = this.elapsedMs;
290
+ if (!result.ok) return;
291
+ const expectedMs = expectedDurationMs(result.totalFiles);
292
+ if (elapsed >= expectedMs) {
293
+ this.stream.write(
294
+ yellow(
295
+ `
296
+ That took ${formatDuration(elapsed)} \u2014 longer than we expect for ${countWithCommas(result.totalFiles)} files.
297
+ If it felt like it hung, that is worth reporting: ${SUPPORT_EMAIL}
298
+ `,
299
+ this.color
300
+ )
301
+ );
302
+ } else if (elapsed >= SLOW_RUN_MS) {
303
+ this.stream.write(
304
+ yellow(
305
+ `
306
+ Took ${formatDuration(elapsed)}.
307
+ If this is slower than usual for your project, tell us: ${SUPPORT_EMAIL}
308
+ `,
309
+ this.color
310
+ )
311
+ );
312
+ }
313
+ }
314
+ /** Exposed for the CLI's "nothing to do" fast path. */
315
+ get didAnnounce() {
316
+ return this.announcedExpectation;
317
+ }
318
+ };
319
+
320
+ // src/cli/index.ts
321
+ function collectList(value, previous) {
322
+ const out = previous ? [...previous] : [];
323
+ for (const part of value.split(",")) {
324
+ const trimmed = part.trim();
325
+ if (trimmed.length > 0) out.push(trimmed);
326
+ }
327
+ return out;
328
+ }
329
+ function requireDir(p) {
330
+ const abs = path.resolve(p);
331
+ if (!existsSync(abs) || !statSync(abs).isDirectory()) {
332
+ console.error(`\u2717 not a directory: ${abs}`);
333
+ process.exit(1);
334
+ }
335
+ return abs;
336
+ }
337
+ async function cmdIndex(dir, opts) {
338
+ const abs = dir ? requireDir(dir) : process.cwd();
339
+ const S = colorEnabled({ isTTY: process.stdout.isTTY });
340
+ const { maybeRunFirstRunSetup } = await import("./setup-wizard-G5YYR5T6.js");
341
+ await maybeRunFirstRunSetup({ noSetup: opts.noSetup === true });
342
+ const pid = getProjectId(abs);
343
+ {
344
+ const f = {
345
+ force: opts.force === true,
346
+ changed: opts.changed === true,
347
+ resume: opts.resume === true,
348
+ post: opts.post === true,
349
+ skipPost: opts.skipPostProcessing === true,
350
+ file: opts.file != null,
351
+ imp: opts.import != null
352
+ };
353
+ const LEGAL = [
354
+ { when: (x) => !x.force && !x.changed && !x.resume && !x.post && !x.skipPost && !x.file && !x.imp, behavior: "full index + all post-processing" },
355
+ { when: (x) => x.changed && !x.force && !x.resume && !x.post && !x.skipPost && !x.file && !x.imp, behavior: "incremental parse + bounded post-pass" },
356
+ { when: (x) => x.resume && !x.force && !x.changed && !x.post && !x.skipPost && !x.file && !x.imp, behavior: "incremental from checkpoint + bounded post-pass" },
357
+ { when: (x) => x.post && !x.force && !x.changed && !x.resume && !x.skipPost && !x.file && !x.imp, behavior: "no parse; re-run 5 derived phases only" },
358
+ { when: (x) => x.skipPost && !x.force && !x.changed && !x.resume && !x.post && !x.file && !x.imp, behavior: "parse+store only; derived data deferred (bb index --post to finish)" },
359
+ { when: (x) => x.file && !x.force && !x.changed && !x.resume && !x.post && !x.skipPost && !x.imp, behavior: "single-file reindex + scoped post-pass" },
360
+ { when: (x) => x.imp && !x.force && !x.changed && !x.resume && !x.post && !x.skipPost && !x.file, behavior: "adopt DB, then incremental top-up" },
361
+ { when: (x) => x.force && !x.changed && !x.resume && !x.post && !x.skipPost && !x.file && !x.imp, behavior: "wipe + full index + full post-processing" }
362
+ ];
363
+ if (!LEGAL.some((l) => l.when(f))) {
364
+ const active = Object.entries(f).filter(([, v]) => v).map(([k]) => `--${k === "skipPost" ? "skip-post-processing" : k === "imp" ? "import" : k}`).join(" ");
365
+ console.error(`\u2717 conflicting bb index options: ${active || "(none)"}
366
+ This combination has no defined behavior. See the semantics matrix: bb index --help, docs/m40/PLAN.md
367
+ Remediations: --force alone rebuilds everything; --post alone re-runs derived-data phases; --changed/--resume alone run the bounded pass.`);
368
+ process.exitCode = 1;
369
+ return;
370
+ }
371
+ }
372
+ if (opts.concurrency !== void 0) {
373
+ const n = Number(opts.concurrency);
374
+ if (!Number.isInteger(n) || n < 1 || n > 64) {
375
+ console.error(`\u2717 --concurrency expects an integer 1-64 (got ${JSON.stringify(opts.concurrency)})`);
376
+ process.exitCode = 1;
377
+ return;
378
+ }
379
+ ParallelIndexerConfig.concurrency = n;
380
+ }
381
+ const excludeGlobs = Array.isArray(opts.exclude) ? opts.exclude : [];
382
+ let resolvedScopedFile = null;
383
+ if (opts.file) {
384
+ const resolved = resolveScopedProjectFile(abs, opts.file);
385
+ if (!resolved.ok) {
386
+ console.error(`\u2717 ${describeScopedPathRefusal(resolved)}`);
387
+ process.exitCode = 1;
388
+ return;
389
+ }
390
+ resolvedScopedFile = resolved.absolute;
391
+ }
392
+ if (opts.post) {
393
+ const { runPostProcessing } = await import("./parallel-indexer-FK2XKZXY.js");
394
+ try {
395
+ const post = await runPostProcessing(pid);
396
+ console.log(`\u2713 post-processing complete in ${(post.totalMs / 1e3).toFixed(1)}s`);
397
+ for (const p of post.phases) {
398
+ console.log(` ${p.outcome === "ok" ? "\u2713" : p.outcome === "failed" ? "\u2717" : "-"} ${p.name.padEnd(14)} ${p.ms}ms`);
399
+ }
400
+ const failedPhases = post.phases.filter((p) => p.outcome === "failed");
401
+ if (failedPhases.length > 0) {
402
+ console.error(`\u2717 ${failedPhases.length} post-processing phase(s) failed (${failedPhases.map((p) => p.name).join(",")}) \u2014 index marked partial`);
403
+ process.exitCode = 1;
404
+ }
405
+ } catch (e) {
406
+ console.error(`\u2717 post-processing failed: ${e?.message}`);
407
+ process.exitCode = 1;
408
+ }
409
+ return;
410
+ }
411
+ if (opts.import != null) {
412
+ const { importDbFile } = await import("./project-registry-ZJ3DVUGO.js");
413
+ const result = importDbFile(opts.import, pid, abs);
414
+ if (result.adopted) {
415
+ console.log(`imported ${opts.import} as project ${pid.slice(0, 8)} (index rebound to this checkout)`);
416
+ return;
417
+ }
418
+ console.error(`\u2717 import failed: ${result.reason}`);
419
+ process.exitCode = 1;
420
+ return;
421
+ }
422
+ let adopted = false;
423
+ if (!opts.force) {
424
+ const dbPath = BrainDatabase.getDbPath(pid);
425
+ if (!existsSync(dbPath)) {
426
+ const { findAdoptableDb, adoptProject } = await import("./project-registry-ZJ3DVUGO.js");
427
+ const { computeCheckoutFingerprint } = await import("./fingerprint-OBLIMZLP.js");
428
+ const cf = await computeCheckoutFingerprint(abs);
429
+ if (cf.fingerprint) {
430
+ const candidate = findAdoptableDb(cf.fingerprint);
431
+ if (candidate) {
432
+ const result = adoptProject(candidate.id, pid, abs);
433
+ if (result.adopted) {
434
+ adopted = true;
435
+ console.log(`adopted existing index from project ${candidate.id.slice(0, 8)} (moved/rebound to ${pid.slice(0, 8)})`);
436
+ } else {
437
+ console.warn(`adoption failed: ${result.reason}`);
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ console.log(bold(`indexing ${abs} (project ${pid.slice(0, 8)}\u2026)`, S));
444
+ if (process.stdout.isTTY) console.log(dim(" scanning project files\u2026", S));
445
+ let heldLock = null;
446
+ if (opts.force) {
447
+ try {
448
+ heldLock = await acquireIndexLock(pid, { kind: "wipe+index" });
449
+ } catch (e) {
450
+ if (e instanceof IndexLockHeldError) {
451
+ console.error(`\u2717 ${e.message}`);
452
+ } else {
453
+ console.error(`\u2717 could not acquire the index lock: ${e?.message}`);
454
+ }
455
+ process.exitCode = 1;
456
+ return;
457
+ }
458
+ try {
459
+ const { wipeProjectData } = await import("./parallel-indexer-FK2XKZXY.js");
460
+ const raw = await BrainDatabase.getRawInstance(pid);
461
+ const wiped = wipeProjectData(raw);
462
+ console.log(`wiped ${pid.slice(0, 8)} index in place (file kept)`);
463
+ const { reclaimSpace, formatSpace, shouldVacuumAfterWipe } = await import("./index-space-IUUN5WYV.js");
464
+ const dbBytes = statSync(BrainDatabase.getDbPath(pid)).size;
465
+ const space = reclaimSpace(raw, BrainDatabase.getDbPath(pid), {
466
+ vacuum: shouldVacuumAfterWipe({ rowsDeleted: wiped.rowsDeleted, dbBytes }),
467
+ vacuumAboveRatio: 0,
468
+ vacuumAboveBytes: 0,
469
+ log: (message, meta) => console.log(` ${message}${meta ? ` ${JSON.stringify(meta)}` : ""}`)
470
+ });
471
+ console.log(` index space: ${formatSpace(space)}`);
472
+ } catch (e) {
473
+ console.error(`\u2717 wipe failed: ${e?.message}`);
474
+ }
475
+ }
476
+ const t = Date.now();
477
+ const indexOpts = { onProgress: (s) => {
478
+ } };
479
+ if (excludeGlobs.length > 0) indexOpts.excludePatterns = excludeGlobs;
480
+ if (opts.skipPostProcessing) indexOpts.skipPostProcessing = true;
481
+ if (opts.file) {
482
+ indexOpts.files = [resolvedScopedFile];
483
+ indexOpts.isIncremental = true;
484
+ }
485
+ if (opts.changed) indexOpts.isIncremental = true;
486
+ if (adopted) indexOpts.isIncremental = true;
487
+ if (opts.resume) {
488
+ const { readLiveStatus } = await import("./index-status-7OVGYSGW.js");
489
+ const ck = readLiveStatus(pid);
490
+ const inProgress = !!ck && (ck.phase === "indexing" || ck.phase === "cancelled") && Date.now() - ck.updatedAt < 36e5;
491
+ if (inProgress) {
492
+ console.log(`resuming interrupted index (checkpoint: ${ck.processedFiles}/${ck.totalFiles} files done)`);
493
+ } else {
494
+ console.log("no in-progress checkpoint; running incremental (only missing/changed files)");
495
+ }
496
+ indexOpts.isIncremental = true;
497
+ }
498
+ const progress = new IndexProgressRenderer({
499
+ stream: process.stderr,
500
+ isTTY: Boolean(process.stderr.isTTY),
501
+ startedAt: t,
502
+ totalFiles: 0
503
+ });
504
+ indexOpts.onProgress = (s) => {
505
+ progress.update({
506
+ processedFiles: Number(s.processedFiles ?? 0),
507
+ totalFiles: Number(s.totalFiles ?? 0),
508
+ skippedFiles: Number(s.skippedFiles ?? 0),
509
+ errorFiles: Number(s.errorFiles ?? 0),
510
+ currentFile: s.currentFile ?? "",
511
+ filesPerSecond: typeof s.filesPerSecond === "number" ? s.filesPerSecond : void 0,
512
+ symbols: typeof s.symbols === "number" ? s.symbols : void 0,
513
+ relationships: typeof s.relationships === "number" ? s.relationships : void 0,
514
+ embeddings: typeof s.embeddings === "number" ? s.embeddings : void 0
515
+ });
516
+ };
517
+ const abort = new AbortController();
518
+ const onSignal = (sig) => {
519
+ if (abort.signal.aborted) return;
520
+ abort.abort();
521
+ console.log(`
522
+ ${sig} received \u2014 cancelling index (draining in-flight files; resume with: bb index --resume)...`);
523
+ };
524
+ if (heldLock) indexOpts.lock = heldLock;
525
+ let stats;
526
+ try {
527
+ process.once("SIGINT", () => onSignal("SIGINT"));
528
+ process.once("SIGTERM", () => onSignal("SIGTERM"));
529
+ indexOpts.signal = abort.signal;
530
+ stats = await indexProjectParallel(pid, abs, indexOpts);
531
+ } catch (e) {
532
+ process.removeAllListeners("SIGINT");
533
+ process.removeAllListeners("SIGTERM");
534
+ if (heldLock) await heldLock.release().catch(() => {
535
+ });
536
+ if (e instanceof IndexLockHeldError) {
537
+ console.error(`\u2717 ${e.message}`);
538
+ } else if (abort.signal.aborted) {
539
+ progress.clear();
540
+ console.log("\u23F8 index cancelled before parsing started \u2014 no files were written. Re-run when ready: bb index");
541
+ process.exitCode = 130;
542
+ return;
543
+ } else {
544
+ throw e;
545
+ }
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+ process.removeAllListeners("SIGINT");
550
+ process.removeAllListeners("SIGTERM");
551
+ progress.clear();
552
+ const secs = ((Date.now() - t) / 1e3).toFixed(1);
553
+ const db = await BrainDatabase.getRawInstance(pid);
554
+ const { pendingEmbeddingCount, embeddingDrainPending, flushBackgroundEmbeddings } = await import("./parallel-indexer-FK2XKZXY.js");
555
+ const pendingAtEnd = pendingEmbeddingCount();
556
+ if (embeddingDrainPending()) {
557
+ const waitMs = Number(process.env.BB_EMBEDDING_DRAIN_WAIT_MS || 3e5);
558
+ const waitSecs = Math.round(waitMs / 1e3);
559
+ console.log(
560
+ pendingAtEnd > 0 ? ` finishing ${pendingAtEnd.toLocaleString()} embedding(s) \u2014 the index and its relationships are already complete; these are vectors (waiting up to ${waitSecs}s)` : ` finishing embedding work \u2014 the index and its relationships are already complete; vectors are still being written (waiting up to ${waitSecs}s)`
561
+ );
562
+ const drain = await flushBackgroundEmbeddings({ timeoutMs: waitMs });
563
+ if (drain.resolved === "drained") {
564
+ console.log(" \u2713 embedding queue drained");
565
+ } else {
566
+ console.log(
567
+ ` \u26A0 ${drain.pendingItems.toLocaleString()} embedding(s) still queued after ${waitSecs}s \u2014 semantic search is partial for those symbols; run \`bb index --changed\` to finish them`
568
+ );
569
+ }
570
+ }
571
+ const files = db.prepare("SELECT COUNT(*) c FROM brain_file").get().c;
572
+ const symbols = db.prepare("SELECT COUNT(*) c FROM brain_symbol").get().c;
573
+ const rels = db.prepare("SELECT COUNT(*) c FROM brain_relationship").get().c;
574
+ const emb = db.prepare("SELECT COUNT(*) c FROM brain_semantic_embedding").get().c;
575
+ const { reclaimSpace: reclaimEndSpace, formatSpace: formatEndSpace } = await import("./index-space-IUUN5WYV.js");
576
+ const endSpace = reclaimEndSpace(db, BrainDatabase.getDbPath(pid), { vacuum: false });
577
+ console.log(dim(` index space: ${formatEndSpace(endSpace)}`, S));
578
+ if (!stats.cancelled) {
579
+ try {
580
+ const { runIndexHealthCheck } = await import("./brain-index-health-Q5APTJQL.js");
581
+ const finalHealth = await runIndexHealthCheck(pid);
582
+ const problemChecks = finalHealth.checks.filter((c) => c.status === "error" || c.status === "warn");
583
+ if (problemChecks.length === 0) {
584
+ console.log(green(` \u2705 index health (after embeddings): ${finalHealth.overallHealth}% \u2014 all checks passed`, S));
585
+ } else {
586
+ console.log(
587
+ yellow(` \u26A0\uFE0F index health (after embeddings): ${finalHealth.overallHealth}% \u2014 ${problemChecks.length} warning(s) (run bb health for details)`, S)
588
+ );
589
+ for (const c of finalHealth.checks) {
590
+ if (c.status === "error") console.log(red(` \u274C ${c.name}: ${c.description}`, S));
591
+ }
592
+ }
593
+ } catch (healthErr) {
594
+ console.log(` \u26A0 index health could not be re-checked after the embedding drain: ${healthErr?.message ?? healthErr}`);
595
+ }
596
+ }
597
+ const parsedNothing = Number(stats.summary?.newlyParsed ?? 0) === 0 && !stats.cancelled;
598
+ const missing = Number(stats.summary?.missingAfterRecovery ?? 0);
599
+ const storeFailures = Number(stats.summary?.storeFailures ?? 0);
600
+ if (missing > 0) {
601
+ console.log(
602
+ yellow(
603
+ `\u26A0 indexed ${files} files \u2014 but ${missing} of the ${stats.totalFiles} discovered file(s) could NOT be stored (${storeFailures} store batch(es) rolled back). The index is INCOMPLETE.`,
604
+ S
605
+ )
606
+ );
607
+ console.log(
608
+ dim(` This is a write failure, not a parse failure: check free disk space and that the database is writable, then re-run.`, S)
609
+ );
610
+ process.exitCode = 1;
611
+ } else if (parsedNothing) {
612
+ const unchanged = Number(stats.summary?.unchanged ?? 0);
613
+ const skipped = Number(stats.summary?.skipped ?? 0);
614
+ console.log(
615
+ green(`\u2713 nothing to index \u2014 already up to date (${unchanged} unchanged, ${skipped} skipped, ${secs}s)`, S)
616
+ );
617
+ } else {
618
+ console.log(green(bold(`\u2713 indexed ${files} files, ${symbols} symbols, ${rels} relationships, ${emb} embeddings in ${secs}s`, S), S));
619
+ if (process.stdout.isTTY) console.log(dim(" Evidence is the answer \u2014 every number above was recorded by this run.", S));
620
+ }
621
+ if (missing === 0 && storeFailures > 0) {
622
+ console.log(dim(` note: ${storeFailures} store batch(es) rolled back and were recovered \u2014 the index is complete`, S));
623
+ }
624
+ console.log(dim(` stats: ${JSON.stringify({ totalFiles: stats.totalFiles, processed: stats.processedFiles, errors: stats.errorFiles })}`, S));
625
+ const embOutcome = stats.summary?.embedOutcome;
626
+ if (embOutcome && (embOutcome.failed > 0 || embOutcome.invalid > 0 || embOutcome.dropped > 0)) {
627
+ const parts = [];
628
+ if (embOutcome.embedded > 0) parts.push(`${embOutcome.embedded} embedded`);
629
+ if (embOutcome.failed > 0) parts.push(`${embOutcome.failed} failed`);
630
+ if (embOutcome.invalid > 0)
631
+ parts.push(`${embOutcome.invalid} discarded as malformed (non-finite or empty)`);
632
+ if (embOutcome.dropped > 0) parts.push(`${embOutcome.dropped} dropped (queue cap)`);
633
+ console.log(` embeddings incomplete: ${parts.join(", ")}`);
634
+ console.log(` semantic search is degraded \u2014 run \`bb doctor\` to check the embedding provider`);
635
+ }
636
+ const embModels = db.prepare(
637
+ `SELECT embedding_model AS model, vector_dimension AS dimension, COUNT(*) AS count
638
+ FROM brain_semantic_embedding
639
+ GROUP BY embedding_model, vector_dimension
640
+ ORDER BY count DESC`
641
+ ).all();
642
+ if (embModels.length > 1) {
643
+ console.log(
644
+ ` \u26A0 index holds ${embModels.length} embedding models \u2014 bb_ask searches only one, so the rest are ignored:`
645
+ );
646
+ for (const m of embModels) {
647
+ console.log(` ${m.model} (${m.dimension}d): ${m.count.toLocaleString()} vectors`);
648
+ }
649
+ console.log(` fix: reindex with one provider \u2014 \`bb index --force\``);
650
+ } else if (embModels.length === 1) {
651
+ console.log(` embeddings: ${embModels[0].model} (${embModels[0].dimension}d)`);
652
+ }
653
+ const integ = stats.summary?.embeddingIntegrity;
654
+ if (integ) {
655
+ if (integ.unusableEmbeddings > 0) {
656
+ console.log(
657
+ ` \u26A0 ${integ.unusableEmbeddings} of ${integ.total.toLocaleString()} vectors are unusable (${integ.dimensionMismatches} length mismatch, ${integ.malformedVectors} malformed)`
658
+ );
659
+ console.log(
660
+ ` these are counted as coverage but unreachable by search \u2014 fix: \`bb index --force\``
661
+ );
662
+ }
663
+ if (integ.truncated) {
664
+ console.log(
665
+ ` (integrity census scanned ${(integ.scanned ?? 0).toLocaleString()} of ${integ.total.toLocaleString()} vectors)`
666
+ );
667
+ }
668
+ }
669
+ progress.finish({
670
+ ok: !stats.cancelled,
671
+ totalFiles: Number(stats.totalFiles ?? 0),
672
+ errorCount: Number(stats.errorFiles ?? 0),
673
+ cancelled: Boolean(stats.cancelled)
674
+ });
675
+ if (stats.cancelled) {
676
+ console.log(`\u23F8 index cancelled after ${secs}s \u2014 progress saved. Resume with: bb index --resume`);
677
+ }
678
+ if (stats.summary) {
679
+ const s = stats.summary;
680
+ console.log(` breakdown: ${s.newlyParsed} newly parsed | ${s.unchanged} unchanged | ${s.skipped} skipped | ${s.symbolLoss} symbol-loss (parsed 0 symbols) | ${s.errors} errors | ${s.timeouts} timeouts${s.cancelled ? ` | ${s.cancelled} cancelled` : ""}`);
681
+ const parseMode = s.parseMode;
682
+ const parsedAnything = Number(s.newlyParsed ?? 0) > 0;
683
+ if (!parsedAnything) {
684
+ console.log(" parsed nothing \u2014 no files needed parsing, so no parse mode was used");
685
+ } else if (parseMode === "in-process") {
686
+ console.log(" parsed in-process \u2014 the worker pool was unavailable, so this run was serial and is correspondingly slower");
687
+ } else if (parseMode === "worker-pool") {
688
+ console.log(" parsed with the worker pool");
689
+ } else {
690
+ console.log(" parsed: mode not recorded for this run (pre-dates the parseMode field)");
691
+ }
692
+ const derived = s.derivedPass ?? "deferred";
693
+ const unresolved = Number(s.unresolvedRelationships ?? -1);
694
+ const derivedNote = {
695
+ full: "derived data fully re-processed",
696
+ bounded: "derived data re-processed for touched files (bounded pass)",
697
+ deferred: "derived data deferred \u2014 run: bb index --post",
698
+ "skipped-unchanged": "no files parsed, so no derived pass ran",
699
+ // A derived phase threw. MEASURED with a fault injected into the resolution
700
+ // pass: the run used to print "full (derived data fully re-processed)" with
701
+ // a WARN in the log and exit 0, and `bb index --post` printed the same
702
+ // condition as a success. Same remediation as stale derived data.
703
+ partial: "at least one derived phase FAILED, so the derived data is incomplete"
704
+ };
705
+ console.log(` derived: ${derived} (${derivedNote[derived] ?? derived})`);
706
+ if (derived === "partial") {
707
+ console.error(" \u2717 a derived phase failed \u2014 index marked partial. Run: bb index --post");
708
+ process.exitCode = 1;
709
+ }
710
+ if (unresolved === 0) {
711
+ console.log(
712
+ " consistency: every internal relationship is attached to a symbol (external references are attached to an external identity, which is correct)"
713
+ );
714
+ } else if (unresolved > 0) {
715
+ console.log(
716
+ ` consistency: ${unresolved} internal relationship(s) are attached to neither a symbol nor an external reference \u2014 they are counted above but read as edges by no traversal. Run: bb index --post`
717
+ );
718
+ } else {
719
+ console.log(" consistency: not measured for this run (pre-dates the unresolved-relationship count)");
720
+ }
721
+ if (stats.m40Bounded) {
722
+ const b = stats.m40Bounded;
723
+ const parts = [`${b.touchedSymbols} symbols touched`];
724
+ if (b.repairedInEdges > 0) parts.push(`${b.repairedInEdges} cross-file edges restored`);
725
+ if (b.purgedDeletedFiles > 0) parts.push(`${b.purgedDeletedFiles} deleted files purged`);
726
+ console.log(` bounded: ${parts.join(" | ")}`);
727
+ }
728
+ }
729
+ if (heldLock) await heldLock.release().catch(() => {
730
+ });
731
+ }
732
+ async function cmdSearch(query, opts) {
733
+ const dir = opts.dir ? requireDir(opts.dir) : process.cwd();
734
+ const pid = getProjectId(dir);
735
+ const dbPath = BrainDatabase.getDbPath(pid);
736
+ if (!existsSync(dbPath)) {
737
+ console.error(`\u2717 no index for ${dir}. Run: bb index "${dir}"`);
738
+ process.exit(1);
739
+ }
740
+ const KNOWN_MODES = /* @__PURE__ */ new Set(["symbols", "content", "symbol", "file"]);
741
+ if (opts.mode !== void 0 && !KNOWN_MODES.has(opts.mode)) {
742
+ console.error(`\u2717 unknown search mode "${opts.mode}" (known: symbols, content, symbol, file)`);
743
+ process.exit(1);
744
+ }
745
+ if (opts.language !== void 0) {
746
+ const { ASTEngine } = await import("./engine-VIW6NSKO.js");
747
+ if (!ASTEngine.isSupportedLanguage(opts.language)) {
748
+ console.error(
749
+ `\u2717 unknown language "${opts.language}". Run \`bb index --help\` for supported languages, or \`bb projects\` to see which are in this index.`
750
+ );
751
+ process.exit(1);
752
+ }
753
+ }
754
+ const mode = opts.mode ?? "symbols";
755
+ if (mode !== "symbols") {
756
+ const { createToolHandlers } = await import("./tool-handlers-OJDC35DI.js");
757
+ const handler = createToolHandlers()["bb_search"];
758
+ const args = mode === "file" ? { mode: "file", filePath: opts.filePath ?? query } : mode === "symbol" ? { mode: "symbol", symbolName: opts.symbolName ?? query, filePath: void 0, ...opts.noAutoRefresh ? { refresh: false } : {} } : { mode: "content", query, limit: opts.limit ?? 15, language: opts.language, ...opts.noAutoRefresh ? { refresh: false } : {} };
759
+ const res = await handler(args, { projectId: pid, projectPath: dir });
760
+ if (res.isError) {
761
+ console.error(res.content.map((c) => c.text).join("\n"));
762
+ process.exitCode = 1;
763
+ return;
764
+ }
765
+ const parsed = JSON.parse(res.content[0].text);
766
+ if (mode === "content") {
767
+ for (const r of parsed.results ?? []) {
768
+ const stale = r.stale ? " \u26A0stale" : "";
769
+ console.log(` ${path.posix.basename(String(r.fileId ?? r.file ?? ""))}:${r.line ?? "?"} \u2014 ${String(r.snippet ?? r.text ?? "").split("\n")[0].slice(0, 120)}${stale}`);
770
+ }
771
+ const env = parsed.freshness ?? {};
772
+ console.log(`
773
+ ${parsed.total} results${parsed.staleCount ? ` (${parsed.staleCount} stale)` : ""}${env.stale != null ? ` | freshness: ${env.fresh} fresh / ${env.stale} stale / ${env.missing} missing` : ""}${parsed.staleCount ? ` \u2014 ${FRESHNESS_REMEDIATION.dir}` : ""}`);
774
+ } else if (mode === "symbol") {
775
+ for (const m of parsed.matches ?? []) {
776
+ const stale = m.stale ? " \u26A0stale" : "";
777
+ const symType = m.symbolType ?? m.type;
778
+ const line = m.lineStart ?? m.line;
779
+ const file = m.fileId ?? m.file ?? "";
780
+ const lang = m.language ?? "";
781
+ const loc = lang ? `${lang}, ${path.posix.basename(String(file))}:${line ?? "?"}` : `${path.posix.basename(String(file))}:${line ?? "?"}`;
782
+ console.log(` ${m.name}${symType ? ` [${symType}]` : ""} (${loc})${stale}`);
783
+ }
784
+ const env = parsed.freshness ?? {};
785
+ if (env.stale != null) console.log(`
786
+ freshness: ${env.fresh} fresh / ${env.stale} stale / ${env.missing} missing \u2014 ${env.remediation ?? ""}`);
787
+ if ((parsed.matches ?? []).length === 0) console.log(`no results for "${opts.symbolName ?? query}"`);
788
+ } else {
789
+ console.log(`${parsed.file} (${parsed.language})${parsed.truncated ? " [truncated]" : ""}${parsed.stale ? " \u26A0stale" : ""}
790
+
791
+ ${parsed.content}`);
792
+ const env = parsed.freshness ?? {};
793
+ if (env.stale != null) console.log(`
794
+ freshness: ${env.fresh} fresh / ${env.stale} stale / ${env.missing} missing \u2014 ${env.remediation ?? ""}`);
795
+ }
796
+ if (typeof parsed.autoRefresh === "string" && parsed.autoRefresh.length) console.log(parsed.autoRefresh);
797
+ return;
798
+ }
799
+ const runPass = async () => {
800
+ const results = await BrainTools.searchCode(pid, query, {
801
+ maxResults: opts.limit ?? 15,
802
+ languages: opts.language ? [opts.language] : void 0
803
+ });
804
+ const lines = [];
805
+ for (const r of results) {
806
+ const stale = r.stale ? " \u26A0stale" : "";
807
+ lines.push(` ${r.symbolName} [${r.symbolType}] (${r.language}, ${r.file}:${r.line})${stale}`);
808
+ }
809
+ return { results, lines };
810
+ };
811
+ const pass1 = await runPass();
812
+ if (pass1.results.length === 0) {
813
+ console.log(`no results for "${query}"`);
814
+ return;
815
+ }
816
+ let footer = `
817
+ ${pass1.results.length} results${pass1.results.staleCount ? ` (${pass1.results.staleCount} stale \u2014 ${FRESHNESS_REMEDIATION.dir})` : ""}`;
818
+ let decision = null;
819
+ if ((pass1.results.staleCount ?? 0) > 0 && !opts.noAutoRefresh) {
820
+ const raw = await BrainDatabase.getRawInstance(pid);
821
+ const rows = pass1.results.map((r) => ({ fileId: r.fileId }));
822
+ const detail = resolveRowsFreshnessDetailed(raw, pid, rows);
823
+ const staleFiles = collectStaleFiles(raw, rows, detail.statuses);
824
+ if (staleFiles.length > 0) {
825
+ const d = await maybeAutoRefresh(pid, dir, staleFiles, { perCallOptOut: false });
826
+ if (d.action === "refreshed") {
827
+ const pass2 = await runPass();
828
+ decision = autoRefreshLine(d);
829
+ for (const line of pass2.lines) console.log(line);
830
+ if (pass2.results.length === 0) console.log(`no results for "${query}"`);
831
+ else console.log(`
832
+ ${pass2.results.length} results`);
833
+ console.log(decision);
834
+ return;
835
+ }
836
+ if (d.action === "declined") decision = autoRefreshLine(d);
837
+ }
838
+ }
839
+ for (const line of pass1.lines) console.log(line);
840
+ console.log(footer);
841
+ if (decision && !footer.includes(decision)) console.log(decision);
842
+ }
843
+ async function cmdStats(dir, opts = {}) {
844
+ const abs = dir ? requireDir(dir) : process.cwd();
845
+ const pid = getProjectId(abs);
846
+ const { composeStatus } = await import("./index-status-7OVGYSGW.js");
847
+ const s = await composeStatus(pid);
848
+ if (!s.hasIndex) {
849
+ if (opts.json) console.log(JSON.stringify({ dir: abs, projectId: pid, hasIndex: false }, null, 2));
850
+ else console.error(`no index for ${abs}. Run: bb index "${abs}"`);
851
+ process.exitCode = 1;
852
+ return;
853
+ }
854
+ let embeddings = 0;
855
+ try {
856
+ const r = await BrainDatabase.getRawInstance(pid);
857
+ embeddings = r.prepare("SELECT COUNT(*) c FROM brain_semantic_embedding").get()?.c ?? 0;
858
+ } catch {
859
+ }
860
+ const dbPath = BrainDatabase.getDbPath(pid);
861
+ if (opts.json) {
862
+ console.log(
863
+ JSON.stringify(
864
+ {
865
+ dir: abs,
866
+ projectId: pid,
867
+ hasIndex: true,
868
+ files: s.dbStats.files,
869
+ symbols: s.dbStats.symbols,
870
+ relationships: s.dbStats.relationships,
871
+ embeddings,
872
+ dbPath
873
+ },
874
+ null,
875
+ 2
876
+ )
877
+ );
878
+ return;
879
+ }
880
+ console.log(`index stats for ${abs}:`);
881
+ console.log(` files ${s.dbStats.files} | symbols ${s.dbStats.symbols} | relationships ${s.dbStats.relationships} | embeddings ${embeddings}`);
882
+ console.log(` db: ${dbPath}`);
883
+ }
884
+ async function cmdProjects(opts) {
885
+ const { listProjects, removeProject, totalProjectsBytes } = await import("./project-registry-ZJ3DVUGO.js");
886
+ if (opts.remove) {
887
+ const r = removeProject(opts.remove, opts.force === true);
888
+ if (opts.json) console.log(JSON.stringify({ removed: r.removed, projectId: opts.remove, reason: r.removed ? null : r.reason }, null, 2));
889
+ else if (r.removed) console.log(`removed project ${opts.remove.slice(0, 8)} (index reclaimed)`);
890
+ else console.error(`\u2717 ${r.reason}`);
891
+ process.exitCode = r.removed ? 0 : 1;
892
+ return;
893
+ }
894
+ const projects = listProjects();
895
+ if (projects.length === 0) {
896
+ if (opts.json) console.log(JSON.stringify({ dataDir: bbProjectsDir(), count: 0, totalBytes: 0, projects: [] }, null, 2));
897
+ else console.log("no indexed projects");
898
+ return;
899
+ }
900
+ const sort = opts.sort ?? "name";
901
+ projects.sort((a, b) => sort === "size" ? b.dbSizeBytes - a.dbSizeBytes : sort === "time" ? (b.lastIndexed ?? 0) - (a.lastIndexed ?? 0) : a.id.localeCompare(b.id));
902
+ const mb = (b) => `${(b / 1024 / 1024).toFixed(1)}MB`;
903
+ if (opts.json) {
904
+ console.log(
905
+ JSON.stringify(
906
+ {
907
+ dataDir: bbProjectsDir(),
908
+ count: projects.length,
909
+ totalBytes: totalProjectsBytes(projects),
910
+ projects
911
+ },
912
+ null,
913
+ 2
914
+ )
915
+ );
916
+ return;
917
+ }
918
+ const when = (t) => t ? new Date(t).toISOString().slice(0, 10) : "n/a";
919
+ for (const p of projects) {
920
+ const where = p.humanPath ?? "(no path recorded)";
921
+ const counts = p.unreadable ? `UNREADABLE (${p.unreadable})` : `${p.files} files ${p.symbols} syms`;
922
+ console.log(`${p.id.slice(0, 8)} | ${counts} | ${mb(p.dbSizeBytes)} | idx ${when(p.lastIndexed)} | ${p.status ?? ""} | ${where} | db: ${p.dbPath}`);
923
+ }
924
+ const unreadableCount = projects.filter((p) => p.unreadable !== null).length;
925
+ console.log(`
926
+ ${projects.length} projects, ${mb(totalProjectsBytes(projects))} total index size (${bbProjectsDir()})`);
927
+ if (unreadableCount > 0) {
928
+ console.log(
929
+ `${unreadableCount} of ${projects.length} could not be read; re-index them (bb index <path>) \u2014 their file/symbol counts are unknown, not zero`
930
+ );
931
+ }
932
+ }
933
+ async function cmdStatus(dir, opts = {}) {
934
+ const abs = dir ? requireDir(dir) : process.cwd();
935
+ const pid = getProjectId(abs);
936
+ const { composeStatus } = await import("./index-status-7OVGYSGW.js");
937
+ const s = await composeStatus(pid);
938
+ if (!s.hasIndex) {
939
+ if (opts.json) console.log(JSON.stringify({ dir: abs, projectId: pid, hasIndex: false }, null, 2));
940
+ else console.error(`no index for ${abs}. Run: bb index "${abs}"`);
941
+ process.exitCode = 1;
942
+ return;
943
+ }
944
+ const dbPath = BrainDatabase.getDbPath(pid);
945
+ if (opts.json) {
946
+ console.log(JSON.stringify({ dir: abs, projectId: pid, dbPath, ...s }, null, 2));
947
+ return;
948
+ }
949
+ const ms = (v) => typeof v === "number" ? `${v}ms` : "n/a";
950
+ if (s.isRunning && s.live) {
951
+ const l = s.live;
952
+ const total = l.totalFiles > 0 ? l.totalFiles : l.processedFiles;
953
+ const pct = l.totalFiles > 0 ? Math.floor(l.processedFiles / l.totalFiles * 100) : 0;
954
+ const elapsed = ((Date.now() - l.startedAt) / 1e3).toFixed(0);
955
+ console.log(`\u25B6 indexing ${abs}: ${l.processedFiles}/${total} files (${pct}%) in ${elapsed}s${typeof l.filesPerSecond === "number" ? ` @ ${l.filesPerSecond.toFixed(1)} files/s` : ""}${l.currentFile ? ` \u2014 ${l.currentFile}` : ""}`);
956
+ console.log(` phase: ${l.phase}`);
957
+ } else if (s.lastRun) {
958
+ const r = s.lastRun;
959
+ console.log(`\u2713 last index of ${abs}: ${s.dbStats.files} files, ${s.dbStats.symbols} symbols, ${s.dbStats.relationships} relationships`);
960
+ const t = r.phaseTimings ?? {};
961
+ console.log(` total ${ms(t.totalWallMs)} | parse ${ms(t.parseMs)} | dbFlush ${ms(t.dbFlushMs)} | discovery ${ms(t.discoveryMs)} | startup ${ms(t.workerStartupMs)}`);
962
+ if (Array.isArray(r.postProcessing) && r.postProcessing.length) {
963
+ console.log(` post-processing: ${r.postProcessing.map((p) => `${p.phase ? p.phase : "?"}=${ms(p.ms)}`).join(", ")}`);
964
+ }
965
+ console.log(` mode ${r.mode ?? "n/a"} | workers ${r.workerCount ?? "n/a"} | noop ${r.noop ? "yes" : "no"}`);
966
+ } else {
967
+ console.log(`index meta: ${s.dbStats.files} files, ${s.dbStats.symbols} symbols, ${s.dbStats.relationships} relationships (no recent run)`);
968
+ }
969
+ console.log(` db: ${BrainDatabase.getDbPath(pid)}`);
970
+ }
971
+ async function cmdHealth(dir, opts) {
972
+ const abs = dir ? requireDir(dir) : process.cwd();
973
+ const pid = getProjectId(abs);
974
+ const dbPath = BrainDatabase.getDbPath(pid);
975
+ if (!existsSync(dbPath)) {
976
+ console.error(`\u2717 no index for ${abs}. Run: bb index "${abs}"`);
977
+ process.exit(1);
978
+ }
979
+ const report = await getUnifiedHealth(pid, abs, {
980
+ refresh: opts.refresh === true || opts.force === true,
981
+ force: opts.force === true,
982
+ wait: opts.wait !== false,
983
+ freshness: opts.complete === true ? "complete" : "sampled",
984
+ sampleSize: opts.sampleSize,
985
+ integrity: opts.integrity === true,
986
+ verbose: opts.verboseConfig === true
987
+ });
988
+ console.log(formatUnifiedHealth(report));
989
+ if (report.warnings.some((w) => w.severity === "error")) process.exitCode = 1;
990
+ }
991
+ async function cmdTool(name, opts) {
992
+ const projectPath = opts.dir ? requireDir(opts.dir) : process.cwd();
993
+ const { PUBLIC_TOOL_DESCRIPTORS } = await import("./tool-registry-BPEJNSZG.js");
994
+ const { validateToolArguments } = await import("./tool-validation-Q57YMMBX.js");
995
+ const { createToolHandlers } = await import("./tool-handlers-OJDC35DI.js");
996
+ const descriptor = PUBLIC_TOOL_DESCRIPTORS.find((tool) => tool.name === name);
997
+ if (!descriptor) throw new Error(`unknown public tool ${name}; expected: ${PUBLIC_TOOL_DESCRIPTORS.map((tool) => tool.name).join(", ")}`);
998
+ let args = {};
999
+ try {
1000
+ args = opts.args ? JSON.parse(opts.args) : {};
1001
+ } catch {
1002
+ throw new Error("--args must be a JSON object");
1003
+ }
1004
+ if (!args || Array.isArray(args) || typeof args !== "object") throw new Error("--args must be a JSON object");
1005
+ const validated = validateToolArguments(descriptor, args);
1006
+ if (descriptor.requiresIndex) {
1007
+ const validatedArgs = validated;
1008
+ const mode = typeof validatedArgs.mode === "string" ? validatedArgs.mode : void 0;
1009
+ const indexFree = mode !== void 0 && (descriptor.indexFreeModes ?? []).includes(mode);
1010
+ if (!indexFree) {
1011
+ const { BrainDatabase: BrainDatabase2 } = await import("./database-76RP44JG.js");
1012
+ try {
1013
+ await BrainDatabase2.getRawInstance(getProjectId(projectPath), { readonly: true });
1014
+ } catch {
1015
+ throw new Error(`No index found for ${projectPath}. Run: bb index "${projectPath}" (or restart bb mcp after indexing).`);
1016
+ }
1017
+ }
1018
+ }
1019
+ const handler = createToolHandlers()[name];
1020
+ const result = await handler(validated, { projectPath, projectId: getProjectId(projectPath) });
1021
+ console.log(result.content.map((item) => item.text).join("\n"));
1022
+ if (result.isError) process.exitCode = 1;
1023
+ }
1024
+ async function runCli(argv) {
1025
+ const program = new Command();
1026
+ program.name("bb").description("Backbencher Brain \u2014 evidence is the answer \xB7 code intelligence for 50 languages").version(BACKBENCHER_BRAIN_VERSION, "-v, --version", "output the version number").option("--verbose", "show diagnostics (debug logs, per-file worker/WASM lines)").option("--quiet", "suppress informational output (warnings/errors only)").hook("preAction", (thisCommand) => {
1027
+ const opts = thisCommand.opts();
1028
+ if (opts.verbose) {
1029
+ process.env.BB_LOG_LEVEL = "debug";
1030
+ setLogLevel(1 /* DEBUG */);
1031
+ } else if (opts.quiet) {
1032
+ process.env.BB_LOG_LEVEL = "warn";
1033
+ setLogLevel(3 /* WARN */);
1034
+ }
1035
+ });
1036
+ program.showSuggestionAfterError(true);
1037
+ program.showHelpAfterError("(run 'bb --help' for a list of commands)");
1038
+ program.command("help").description("display help for a command, or list all commands").argument("[command...]", "command to describe; include a subcommand for `bb help <cmd> <sub>` (omit for the command list)").action((commandArg) => {
1039
+ const parts = (Array.isArray(commandArg) ? commandArg : [commandArg]).filter(Boolean);
1040
+ const commandName = parts[0];
1041
+ if (!commandName) {
1042
+ program.outputHelp();
1043
+ return;
1044
+ }
1045
+ const target = program.commands.find((c) => c.name() === commandName);
1046
+ if (!target) {
1047
+ const known = program.commands.map((c) => c.name()).sort();
1048
+ const near = known.filter((n) => n.startsWith(commandName.slice(0, 2)));
1049
+ console.error(`bb help: unknown command "${commandName}" \u2014 no such command.`);
1050
+ if (near.length) console.error(`did you mean: ${near.join(", ")}?`);
1051
+ console.error(`known commands: ${known.join(", ")}`);
1052
+ process.exitCode = 1;
1053
+ return;
1054
+ }
1055
+ let cmd = target;
1056
+ for (const part of parts.slice(1)) {
1057
+ const next = cmd.commands.find((c) => c.name() === part);
1058
+ if (!next) {
1059
+ const subs = cmd.commands.map((c) => c.name()).sort();
1060
+ console.error(`bb help: unknown subcommand "${part}" of "${cmd.name()}" \u2014 no such subcommand.`);
1061
+ if (subs.length) console.error(`its subcommands: ${subs.join(", ")}`);
1062
+ process.exitCode = 1;
1063
+ return;
1064
+ }
1065
+ cmd = next;
1066
+ }
1067
+ cmd.outputHelp();
1068
+ });
1069
+ program.command("tool").description("run one of the public tools with JSON arguments").argument("<name>", "public tool name, e.g. bb_search or bb_doc").argument("[unexpected...]", "rejected: tool arguments are passed with --args '<json>' so a stray positional is an error, not a silent no-op").option("--dir <dir>", "project directory (default: cwd)").option("--args <json>", "tool arguments as a JSON object", "{}").action(async (name, unexpected, opts) => {
1070
+ if (Array.isArray(unexpected) && unexpected.length > 0) {
1071
+ console.error(`\u2717 unexpected argument(s): ${unexpected.map((a) => JSON.stringify(a)).join(" ")}`);
1072
+ console.error(` Tool arguments are a JSON object: bb tool ${name} --args '{"query":"..."}'`);
1073
+ process.exitCode = 1;
1074
+ return;
1075
+ }
1076
+ await cmdTool(name, opts);
1077
+ });
1078
+ program.command("index").description("index a project (default: current directory)").argument("[dir]", "project directory (default: current directory)").option("--force", "wipe the existing index and rebuild").option("--no-setup", "skip the first-run embedding setup prompt").option("--skip-post-processing", "parse + store only; SKIP relationship resolution/enrichment/gravity/taint/health (derived data goes stale until: bb index --post)").option("--post", "re-run relationship/enrichment/gravity/taint/health phases only \u2014 no re-parse (remediation for stale derived data after --changed/--resume/agent edits)").option(
1079
+ "--file <path>",
1080
+ "reindex a single file's symbols + relationships, relative to <dir> or absolute and must be inside the project (runs a scoped derived-data pass for that file; refuses a missing/outside path)"
1081
+ ).option("--changed", "reindex only files changed since the last index (runs the bounded derived-data pass for touched files \u2014 relationships stay current)").option("--resume", "continue from an interrupted run's checkpoint (incremental: only missing/changed files; runs the bounded derived-data pass)").option("--import <db>", "adopt an existing brain.db file (re-home a moved project's index)").option(
1082
+ "--exclude <globs>",
1083
+ "skip files matching comma-separated globs during discovery (repeatable); NON-DESTRUCTIVE: files already indexed by an earlier run keep their rows",
1084
+ collectList,
1085
+ []
1086
+ ).option("--concurrency <n>", "worker count for this run (1-64; default max(2, min(cpus-1, 12)))").addHelpText("after", `
1087
+ Examples:
1088
+ $ bb index # index the current directory
1089
+ $ bb index ../my-project # index another directory
1090
+ $ bb index --changed # reindex only changed files
1091
+ $ bb index --resume # continue an interrupted run from its checkpoint
1092
+ $ bb index --file src/a.ts # reindex a single file
1093
+ $ bb index --post # re-run derived phases only (no re-parse)
1094
+ $ bb index --force # wipe and rebuild in place
1095
+ $ bb index import --db <path> # adopt an existing DB for a moved project
1096
+
1097
+ Interrupted a run? Re-run with --resume; it re-parses only what is missing or
1098
+ changed. Nothing to do? --changed costs seconds where a full run costs minutes.
1099
+ Indexing far slower than expected? Run \`bb status\` for per-phase timings and
1100
+ \`bb doctor\` to check the worker pool.
1101
+
1102
+ Full reference: man bb, or docs/indexing/ in the repository.
1103
+ `).action(async (dir, opts) => {
1104
+ await cmdIndex(dir, { ...opts, noSetup: opts.setup === false });
1105
+ });
1106
+ const setupCmd = program.command("setup").description("embedding provider setup (API key, model)");
1107
+ setupCmd.action(async () => {
1108
+ const { runSetupWizard } = await import("./setup-wizard-G5YYR5T6.js");
1109
+ if (!process.stdin.isTTY) {
1110
+ console.error("\u2717 bb setup requires an interactive terminal.");
1111
+ process.exit(1);
1112
+ }
1113
+ await runSetupWizard();
1114
+ });
1115
+ setupCmd.command("show").description("show the embedding configuration (key as fingerprint only)").action(async () => {
1116
+ const { describeEmbeddingConfig, renderSetupShow } = await import("./setup-show-NT5ZHYNE.js");
1117
+ const state = describeEmbeddingConfig();
1118
+ console.log(renderSetupShow(state));
1119
+ if (!state.configured) process.exitCode = 1;
1120
+ });
1121
+ program.command("doctor").description("diagnose embedding setup and index consistency (read-only)").option("--dir <dir>", "project directory (default: cwd)").option("--fix", "repair permission problems (0600 only; this IS the explicit apply \u2014 without it doctor is read-only)").action(async (opts) => {
1122
+ const { runDoctorChecks, renderDoctorReport, applyDoctorFixes, renderFixPlan } = await import("./doctor-ANGETYMN.js");
1123
+ const cwd = opts.dir ? requireDir(opts.dir) : process.cwd();
1124
+ if (opts.fix) {
1125
+ const { plan, applied } = applyDoctorFixes({ cwd, apply: true });
1126
+ console.log(renderFixPlan(plan, applied));
1127
+ if (plan.refusals.length > 0) process.exitCode = 1;
1128
+ return;
1129
+ }
1130
+ const report = runDoctorChecks({ cwd, projectId: getProjectId(cwd) });
1131
+ console.log(renderDoctorReport(report));
1132
+ if (!report.healthy) process.exitCode = 1;
1133
+ });
1134
+ async function cmdUpgrade(opts) {
1135
+ const { BACKBENCHER_BRAIN_VERSION: current, PKG_NAME } = await import("./version-ABP2TUL2.js");
1136
+ const range = opts.to ?? "latest";
1137
+ const spec = `${PKG_NAME}@${range}`;
1138
+ console.log(`bb ${current}`);
1139
+ const controller = new AbortController();
1140
+ const timer = setTimeout(() => controller.abort(), 2e4);
1141
+ let latest = null;
1142
+ let registryError = null;
1143
+ try {
1144
+ const encoded = encodeURIComponent(range);
1145
+ const isDistTag = /^[a-z][a-z0-9._-]*$/i.test(range) && !/^\d/.test(range);
1146
+ const encodedName = encodeURIComponent(PKG_NAME);
1147
+ const url = isDistTag ? `https://registry.npmjs.org/${encodedName}/${encoded}` : `https://registry.npmjs.org/${encodedName}`;
1148
+ const res = await fetch(url, {
1149
+ signal: controller.signal,
1150
+ headers: { accept: "application/json" }
1151
+ });
1152
+ if (!res.ok) {
1153
+ registryError = `registry returned HTTP ${res.status}`;
1154
+ } else {
1155
+ const body = await res.json();
1156
+ if (isDistTag) {
1157
+ latest = typeof body.version === "string" ? body.version : null;
1158
+ } else {
1159
+ const exact = body.versions?.[range];
1160
+ if (exact) {
1161
+ latest = range;
1162
+ } else {
1163
+ const tag = body["dist-tags"]?.latest;
1164
+ latest = typeof tag === "string" ? tag : null;
1165
+ }
1166
+ }
1167
+ if (!latest) registryError = "registry response had no version field";
1168
+ }
1169
+ } catch (e) {
1170
+ registryError = e?.name === "AbortError" ? "registry request timed out after 20s" : e?.message ?? String(e);
1171
+ } finally {
1172
+ clearTimeout(timer);
1173
+ }
1174
+ if (!latest) {
1175
+ console.log(`could not check for updates: ${registryError}`);
1176
+ console.log(`compare manually: npm view ${spec} version`);
1177
+ console.log(`update with: npm install -g ${spec}`);
1178
+ process.exitCode = 1;
1179
+ return;
1180
+ }
1181
+ if (latest === current) {
1182
+ console.log(`\u2713 up to date (latest published is ${latest})`);
1183
+ return;
1184
+ }
1185
+ console.log(`update available: ${current} -> ${latest}`);
1186
+ console.log(`update with: npm install -g ${spec}`);
1187
+ if (!opts.check) {
1188
+ console.log(`
1189
+ then re-index so the new version's analysis applies:`);
1190
+ console.log(` bb index --changed`);
1191
+ }
1192
+ }
1193
+ program.command("upgrade").description("check for a newer bb and report exactly how to install it").option("--check", "only report whether a newer version exists; change nothing").option("--to <dist-tag-or-range>", "npm dist-tag or version range to compare against (default: latest)").addHelpText(
1194
+ "after",
1195
+ `
1196
+ bb is installed with \`npm install -g @backbencher/brain\`, so npm owns the
1197
+ install and bb does not replace its own files. This command reports the installed
1198
+ version, the newest published version, and the command to run \u2014 it never mutates
1199
+ an install behind npm's back.
1200
+
1201
+ $ bb upgrade --check # is there a newer version? (no network writes)
1202
+ $ bb upgrade # same report, plus the exact command to run
1203
+
1204
+ After upgrading, re-index: $ bb index --changed
1205
+ `
1206
+ ).action(async (opts) => {
1207
+ await cmdUpgrade(opts);
1208
+ });
1209
+ program.command("stats").description("query index counts anytime: files/symbols/relationships/embeddings (default: cwd)").argument("[dir]", "project directory (default: current directory)").option("--json", "machine-readable JSON output (stable keys for scripts)").action(async (dir, opts) => {
1210
+ await cmdStats(dir, opts);
1211
+ });
1212
+ program.command("status").description("show in-flight/last-run index state and per-phase timings (default: cwd)").argument("[dir]", "project directory (default: current directory)").option("--json", "machine-readable JSON output (stable keys for scripts)").action(async (dir, opts) => {
1213
+ await cmdStatus(dir, opts);
1214
+ });
1215
+ program.command("projects").description("list every indexed project: DB path, size, counts, last index (default: all projects)").option("--sort <field>", "sort by name|size|time", "name").option("--remove <id>", "remove one indexed project by id (reclaims its DB + embeddings)").option("--force", "remove even if that project is currently being indexed").option("--json", "machine-readable JSON output for the project list (stable keys for scripts)").addHelpText("after", `
1216
+ Examples:
1217
+ $ bb projects # list all indexed projects with DB path + size
1218
+ $ bb projects --sort size # biggest indexes first
1219
+ $ bb projects --remove 1a2b3c4d # reclaim that project's index space
1220
+ `).action(async (opts) => {
1221
+ await cmdProjects(opts);
1222
+ });
1223
+ program.command("search").description("search the index (modes: symbols [default], content, symbol, file \u2014 parity with the MCP bb_search tool)").argument("[query]", "query text; in file mode, the file path to print (omit when --symbol-name or --file-path is given)").option("--mode <mode>", "retrieval mode: symbols (default), content, symbol, or file").option("--file-path <path>", "file target for --mode file (overrides the positional query)").option("--symbol-name <name>", "exact symbol name for --mode symbol (overrides the positional query)").option("--dir <dir>", "project directory (default: cwd)").option("--limit <n>", "max results", "15").option("--language <lang>", "filter by language").option("--no-auto-refresh", "answer from the existing index with the staleness footer instead of auto-refreshing stale files within budget").addHelpText("after", `
1224
+ Examples:
1225
+ $ bb search handleRequest # symbol listing (default)
1226
+ $ bb search --mode content "findUser(" # content matches with snippets
1227
+ $ bb search --mode symbol --symbol-name handleRequest
1228
+ $ bb search --mode file src/app.ts # print an indexed file's content
1229
+ $ bb search --no-auto-refresh query # never auto-refresh on read
1230
+ `).action(async (query, opts) => {
1231
+ if (!query && !opts.symbolName && !opts.filePath) {
1232
+ console.error(
1233
+ "bb search: nothing to search for. Pass a query, or --symbol-name <name>, or --file-path <path>."
1234
+ );
1235
+ process.exitCode = 1;
1236
+ return;
1237
+ }
1238
+ await cmdSearch(query, { ...opts, noAutoRefresh: opts.autoRefresh === false, limit: parseInt(opts.limit, 10) });
1239
+ });
1240
+ program.command("health").description("index health and freshness").argument("[dir]", "project directory (default: cwd)").option("--refresh", "reindex the project before reporting").option("--force", "force a full in-place reindex (implies --refresh)").option("--no-wait", "do not wait for the refresh to finish").option("--complete", "check freshness for all files (not just a sample)").option("--integrity", "run the 12-point index integrity check").option("--sample-size <n>", "freshness sample size (default: 200)", "200").option("--verbose-config", "show effective embedding config + per-field source (M40.A8)").action(async (dir, opts) => {
1241
+ await cmdHealth(dir, { ...opts, sampleSize: parseInt(opts.sampleSize, 10) });
1242
+ });
1243
+ program.command("onboard").description("write the bb tools guidance section into <dir>/AGENTS.md (generated from the tool registry)").argument("[dir]", "project directory (default: current directory)").option("--check", "report drift/ghost tool names without writing (exit 1 when not current)").option("--print", "print the generated section to stdout without writing").option("--file <path>", "guide file to write (default: <dir>/AGENTS.md)").addHelpText("after", `
1244
+ Examples:
1245
+ $ bb onboard # write/refresh AGENTS.md in the current project
1246
+ $ bb onboard ../my-project # same for another directory
1247
+ $ bb onboard --check # CI: fail when the guidance drifted or names a ghost tool
1248
+ $ bb onboard --print # show the generated section
1249
+ `).action(async (dir, opts) => {
1250
+ const { onboard, renderOnboardResult } = await import("./onboard-DIQZV2NW.js");
1251
+ const abs = dir ? requireDir(dir) : process.cwd();
1252
+ const result = onboard(abs, { check: opts.check === true, print: opts.print === true, file: opts.file });
1253
+ console.log(renderOnboardResult(result));
1254
+ if (opts.check === true && result.changed) process.exitCode = 1;
1255
+ });
1256
+ program.command("mcp").description("start the MCP server over stdio (for Claude Code, Cursor, VS Code\u2026)").option("--dir <dir>", "default project directory (default: cwd)").action(async (opts) => {
1257
+ const { startMcpServer } = await import("./mcp/server.js");
1258
+ await startMcpServer(opts.dir ? requireDir(opts.dir) : process.cwd());
1259
+ });
1260
+ program.command("version").description("print the bb version").action(() => {
1261
+ console.log(BACKBENCHER_BRAIN_VERSION);
1262
+ });
1263
+ await program.parseAsync(argv, { from: "user" });
1264
+ }
1265
+ var isDirectRun = (() => {
1266
+ const m = import.meta;
1267
+ if (typeof m.main === "boolean") return m.main;
1268
+ try {
1269
+ const entry = process.argv[1] ? path.resolve(process.argv[1]) : null;
1270
+ if (!entry) return false;
1271
+ const self = fileURLToPath(m.url ?? import.meta.url);
1272
+ const real = (p) => {
1273
+ try {
1274
+ return realpathSync.native(p);
1275
+ } catch {
1276
+ return p;
1277
+ }
1278
+ };
1279
+ return real(entry) === real(self);
1280
+ } catch {
1281
+ return false;
1282
+ }
1283
+ })();
1284
+ if (isDirectRun) {
1285
+ try {
1286
+ await runCli(process.argv.slice(2));
1287
+ } catch (error) {
1288
+ const message = error instanceof Error ? error.message : String(error);
1289
+ console.error(`error: ${message}`);
1290
+ process.exitCode = 1;
1291
+ }
1292
+ }
1293
+
1294
+ export {
1295
+ runCli
1296
+ };