helixmind 0.1.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.

Potentially problematic release.


This version of helixmind might be problematic. Click here for more details.

Files changed (479) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +207 -0
  3. package/dist/cli/agent/autonomous.d.ts +10 -0
  4. package/dist/cli/agent/autonomous.d.ts.map +1 -0
  5. package/dist/cli/agent/autonomous.js +172 -0
  6. package/dist/cli/agent/autonomous.js.map +1 -0
  7. package/dist/cli/agent/loop.d.ts +76 -0
  8. package/dist/cli/agent/loop.d.ts.map +1 -0
  9. package/dist/cli/agent/loop.js +333 -0
  10. package/dist/cli/agent/loop.js.map +1 -0
  11. package/dist/cli/agent/permissions.d.ts +28 -0
  12. package/dist/cli/agent/permissions.d.ts.map +1 -0
  13. package/dist/cli/agent/permissions.js +180 -0
  14. package/dist/cli/agent/permissions.js.map +1 -0
  15. package/dist/cli/agent/sandbox.d.ts +17 -0
  16. package/dist/cli/agent/sandbox.d.ts.map +1 -0
  17. package/dist/cli/agent/sandbox.js +124 -0
  18. package/dist/cli/agent/sandbox.js.map +1 -0
  19. package/dist/cli/agent/tools/edit-file.d.ts +2 -0
  20. package/dist/cli/agent/tools/edit-file.d.ts.map +1 -0
  21. package/dist/cli/agent/tools/edit-file.js +49 -0
  22. package/dist/cli/agent/tools/edit-file.js.map +1 -0
  23. package/dist/cli/agent/tools/find.d.ts +2 -0
  24. package/dist/cli/agent/tools/find.d.ts.map +1 -0
  25. package/dist/cli/agent/tools/find.js +35 -0
  26. package/dist/cli/agent/tools/find.js.map +1 -0
  27. package/dist/cli/agent/tools/git-commit.d.ts +2 -0
  28. package/dist/cli/agent/tools/git-commit.d.ts.map +1 -0
  29. package/dist/cli/agent/tools/git-commit.js +51 -0
  30. package/dist/cli/agent/tools/git-commit.js.map +1 -0
  31. package/dist/cli/agent/tools/git-diff.d.ts +2 -0
  32. package/dist/cli/agent/tools/git-diff.d.ts.map +1 -0
  33. package/dist/cli/agent/tools/git-diff.js +36 -0
  34. package/dist/cli/agent/tools/git-diff.js.map +1 -0
  35. package/dist/cli/agent/tools/git-log.d.ts +2 -0
  36. package/dist/cli/agent/tools/git-log.d.ts.map +1 -0
  37. package/dist/cli/agent/tools/git-log.js +32 -0
  38. package/dist/cli/agent/tools/git-log.js.map +1 -0
  39. package/dist/cli/agent/tools/git-status.d.ts +2 -0
  40. package/dist/cli/agent/tools/git-status.d.ts.map +1 -0
  41. package/dist/cli/agent/tools/git-status.js +38 -0
  42. package/dist/cli/agent/tools/git-status.js.map +1 -0
  43. package/dist/cli/agent/tools/list-dir.d.ts +2 -0
  44. package/dist/cli/agent/tools/list-dir.d.ts.map +1 -0
  45. package/dist/cli/agent/tools/list-dir.js +73 -0
  46. package/dist/cli/agent/tools/list-dir.js.map +1 -0
  47. package/dist/cli/agent/tools/read-file.d.ts +2 -0
  48. package/dist/cli/agent/tools/read-file.d.ts.map +1 -0
  49. package/dist/cli/agent/tools/read-file.js +45 -0
  50. package/dist/cli/agent/tools/read-file.js.map +1 -0
  51. package/dist/cli/agent/tools/registry.d.ts +18 -0
  52. package/dist/cli/agent/tools/registry.d.ts.map +1 -0
  53. package/dist/cli/agent/tools/registry.js +31 -0
  54. package/dist/cli/agent/tools/registry.js.map +1 -0
  55. package/dist/cli/agent/tools/run-command.d.ts +2 -0
  56. package/dist/cli/agent/tools/run-command.d.ts.map +1 -0
  57. package/dist/cli/agent/tools/run-command.js +79 -0
  58. package/dist/cli/agent/tools/run-command.js.map +1 -0
  59. package/dist/cli/agent/tools/search.d.ts +2 -0
  60. package/dist/cli/agent/tools/search.d.ts.map +1 -0
  61. package/dist/cli/agent/tools/search.js +104 -0
  62. package/dist/cli/agent/tools/search.js.map +1 -0
  63. package/dist/cli/agent/tools/spiral-query.d.ts +2 -0
  64. package/dist/cli/agent/tools/spiral-query.d.ts.map +1 -0
  65. package/dist/cli/agent/tools/spiral-query.js +52 -0
  66. package/dist/cli/agent/tools/spiral-query.js.map +1 -0
  67. package/dist/cli/agent/tools/spiral-store.d.ts +2 -0
  68. package/dist/cli/agent/tools/spiral-store.d.ts.map +1 -0
  69. package/dist/cli/agent/tools/spiral-store.js +37 -0
  70. package/dist/cli/agent/tools/spiral-store.js.map +1 -0
  71. package/dist/cli/agent/tools/web-research.d.ts +2 -0
  72. package/dist/cli/agent/tools/web-research.d.ts.map +1 -0
  73. package/dist/cli/agent/tools/web-research.js +96 -0
  74. package/dist/cli/agent/tools/web-research.js.map +1 -0
  75. package/dist/cli/agent/tools/write-file.d.ts +2 -0
  76. package/dist/cli/agent/tools/write-file.d.ts.map +1 -0
  77. package/dist/cli/agent/tools/write-file.js +91 -0
  78. package/dist/cli/agent/tools/write-file.js.map +1 -0
  79. package/dist/cli/agent/undo.d.ts +30 -0
  80. package/dist/cli/agent/undo.d.ts.map +1 -0
  81. package/dist/cli/agent/undo.js +48 -0
  82. package/dist/cli/agent/undo.js.map +1 -0
  83. package/dist/cli/auth/callback-server.d.ts +15 -0
  84. package/dist/cli/auth/callback-server.d.ts.map +1 -0
  85. package/dist/cli/auth/callback-server.js +168 -0
  86. package/dist/cli/auth/callback-server.js.map +1 -0
  87. package/dist/cli/auth/feature-gate.d.ts +19 -0
  88. package/dist/cli/auth/feature-gate.d.ts.map +1 -0
  89. package/dist/cli/auth/feature-gate.js +74 -0
  90. package/dist/cli/auth/feature-gate.js.map +1 -0
  91. package/dist/cli/auth/guard.d.ts +10 -0
  92. package/dist/cli/auth/guard.d.ts.map +1 -0
  93. package/dist/cli/auth/guard.js +46 -0
  94. package/dist/cli/auth/guard.js.map +1 -0
  95. package/dist/cli/auth/login.d.ts +22 -0
  96. package/dist/cli/auth/login.d.ts.map +1 -0
  97. package/dist/cli/auth/login.js +194 -0
  98. package/dist/cli/auth/login.js.map +1 -0
  99. package/dist/cli/auth/logout.d.ts +6 -0
  100. package/dist/cli/auth/logout.d.ts.map +1 -0
  101. package/dist/cli/auth/logout.js +36 -0
  102. package/dist/cli/auth/logout.js.map +1 -0
  103. package/dist/cli/bench/dataset.d.ts +13 -0
  104. package/dist/cli/bench/dataset.d.ts.map +1 -0
  105. package/dist/cli/bench/dataset.js +97 -0
  106. package/dist/cli/bench/dataset.js.map +1 -0
  107. package/dist/cli/bench/harness.d.ts +7 -0
  108. package/dist/cli/bench/harness.d.ts.map +1 -0
  109. package/dist/cli/bench/harness.js +135 -0
  110. package/dist/cli/bench/harness.js.map +1 -0
  111. package/dist/cli/bench/metrics.d.ts +15 -0
  112. package/dist/cli/bench/metrics.d.ts.map +1 -0
  113. package/dist/cli/bench/metrics.js +98 -0
  114. package/dist/cli/bench/metrics.js.map +1 -0
  115. package/dist/cli/bench/output.d.ts +42 -0
  116. package/dist/cli/bench/output.d.ts.map +1 -0
  117. package/dist/cli/bench/output.js +140 -0
  118. package/dist/cli/bench/output.js.map +1 -0
  119. package/dist/cli/bench/prompt.d.ts +13 -0
  120. package/dist/cli/bench/prompt.d.ts.map +1 -0
  121. package/dist/cli/bench/prompt.js +106 -0
  122. package/dist/cli/bench/prompt.js.map +1 -0
  123. package/dist/cli/bench/runner.d.ts +14 -0
  124. package/dist/cli/bench/runner.d.ts.map +1 -0
  125. package/dist/cli/bench/runner.js +334 -0
  126. package/dist/cli/bench/runner.js.map +1 -0
  127. package/dist/cli/bench/types.d.ts +109 -0
  128. package/dist/cli/bench/types.d.ts.map +1 -0
  129. package/dist/cli/bench/types.js +2 -0
  130. package/dist/cli/bench/types.js.map +1 -0
  131. package/dist/cli/bench/ui.d.ts +12 -0
  132. package/dist/cli/bench/ui.d.ts.map +1 -0
  133. package/dist/cli/bench/ui.js +126 -0
  134. package/dist/cli/bench/ui.js.map +1 -0
  135. package/dist/cli/brain/archive.d.ts +33 -0
  136. package/dist/cli/brain/archive.d.ts.map +1 -0
  137. package/dist/cli/brain/archive.js +159 -0
  138. package/dist/cli/brain/archive.js.map +1 -0
  139. package/dist/cli/brain/control-protocol.d.ts +159 -0
  140. package/dist/cli/brain/control-protocol.d.ts.map +1 -0
  141. package/dist/cli/brain/control-protocol.js +41 -0
  142. package/dist/cli/brain/control-protocol.js.map +1 -0
  143. package/dist/cli/brain/exporter.d.ts +34 -0
  144. package/dist/cli/brain/exporter.d.ts.map +1 -0
  145. package/dist/cli/brain/exporter.js +37 -0
  146. package/dist/cli/brain/exporter.js.map +1 -0
  147. package/dist/cli/brain/generator.d.ts +67 -0
  148. package/dist/cli/brain/generator.d.ts.map +1 -0
  149. package/dist/cli/brain/generator.js +239 -0
  150. package/dist/cli/brain/generator.js.map +1 -0
  151. package/dist/cli/brain/relay-client.d.ts +8 -0
  152. package/dist/cli/brain/relay-client.d.ts.map +1 -0
  153. package/dist/cli/brain/relay-client.js +173 -0
  154. package/dist/cli/brain/relay-client.js.map +1 -0
  155. package/dist/cli/brain/server.d.ts +34 -0
  156. package/dist/cli/brain/server.d.ts.map +1 -0
  157. package/dist/cli/brain/server.js +425 -0
  158. package/dist/cli/brain/server.js.map +1 -0
  159. package/dist/cli/brain/template.d.ts +3 -0
  160. package/dist/cli/brain/template.d.ts.map +1 -0
  161. package/dist/cli/brain/template.js +2072 -0
  162. package/dist/cli/brain/template.js.map +1 -0
  163. package/dist/cli/checkpoints/browser.d.ts +27 -0
  164. package/dist/cli/checkpoints/browser.d.ts.map +1 -0
  165. package/dist/cli/checkpoints/browser.js +238 -0
  166. package/dist/cli/checkpoints/browser.js.map +1 -0
  167. package/dist/cli/checkpoints/keybinding.d.ts +22 -0
  168. package/dist/cli/checkpoints/keybinding.d.ts.map +1 -0
  169. package/dist/cli/checkpoints/keybinding.js +43 -0
  170. package/dist/cli/checkpoints/keybinding.js.map +1 -0
  171. package/dist/cli/checkpoints/revert.d.ts +37 -0
  172. package/dist/cli/checkpoints/revert.d.ts.map +1 -0
  173. package/dist/cli/checkpoints/revert.js +144 -0
  174. package/dist/cli/checkpoints/revert.js.map +1 -0
  175. package/dist/cli/checkpoints/store.d.ts +48 -0
  176. package/dist/cli/checkpoints/store.d.ts.map +1 -0
  177. package/dist/cli/checkpoints/store.js +188 -0
  178. package/dist/cli/checkpoints/store.js.map +1 -0
  179. package/dist/cli/commands/archive.d.ts +7 -0
  180. package/dist/cli/commands/archive.d.ts.map +1 -0
  181. package/dist/cli/commands/archive.js +66 -0
  182. package/dist/cli/commands/archive.js.map +1 -0
  183. package/dist/cli/commands/auth.d.ts +10 -0
  184. package/dist/cli/commands/auth.d.ts.map +1 -0
  185. package/dist/cli/commands/auth.js +44 -0
  186. package/dist/cli/commands/auth.js.map +1 -0
  187. package/dist/cli/commands/bench.d.ts +25 -0
  188. package/dist/cli/commands/bench.d.ts.map +1 -0
  189. package/dist/cli/commands/bench.js +114 -0
  190. package/dist/cli/commands/bench.js.map +1 -0
  191. package/dist/cli/commands/chat.d.ts +11 -0
  192. package/dist/cli/commands/chat.d.ts.map +1 -0
  193. package/dist/cli/commands/chat.js +2321 -0
  194. package/dist/cli/commands/chat.js.map +1 -0
  195. package/dist/cli/commands/config.d.ts +4 -0
  196. package/dist/cli/commands/config.d.ts.map +1 -0
  197. package/dist/cli/commands/config.js +41 -0
  198. package/dist/cli/commands/config.js.map +1 -0
  199. package/dist/cli/commands/feed.d.ts +6 -0
  200. package/dist/cli/commands/feed.d.ts.map +1 -0
  201. package/dist/cli/commands/feed.js +95 -0
  202. package/dist/cli/commands/feed.js.map +1 -0
  203. package/dist/cli/commands/helix-menu.d.ts +4 -0
  204. package/dist/cli/commands/helix-menu.d.ts.map +1 -0
  205. package/dist/cli/commands/helix-menu.js +400 -0
  206. package/dist/cli/commands/helix-menu.js.map +1 -0
  207. package/dist/cli/commands/init.d.ts +2 -0
  208. package/dist/cli/commands/init.d.ts.map +1 -0
  209. package/dist/cli/commands/init.js +26 -0
  210. package/dist/cli/commands/init.js.map +1 -0
  211. package/dist/cli/commands/setup.d.ts +20 -0
  212. package/dist/cli/commands/setup.d.ts.map +1 -0
  213. package/dist/cli/commands/setup.js +314 -0
  214. package/dist/cli/commands/setup.js.map +1 -0
  215. package/dist/cli/commands/spiral.d.ts +4 -0
  216. package/dist/cli/commands/spiral.d.ts.map +1 -0
  217. package/dist/cli/commands/spiral.js +81 -0
  218. package/dist/cli/commands/spiral.js.map +1 -0
  219. package/dist/cli/config/store.d.ts +72 -0
  220. package/dist/cli/config/store.d.ts.map +1 -0
  221. package/dist/cli/config/store.js +241 -0
  222. package/dist/cli/config/store.js.map +1 -0
  223. package/dist/cli/context/assembler.d.ts +8 -0
  224. package/dist/cli/context/assembler.d.ts.map +1 -0
  225. package/dist/cli/context/assembler.js +124 -0
  226. package/dist/cli/context/assembler.js.map +1 -0
  227. package/dist/cli/context/project.d.ts +13 -0
  228. package/dist/cli/context/project.d.ts.map +1 -0
  229. package/dist/cli/context/project.js +126 -0
  230. package/dist/cli/context/project.js.map +1 -0
  231. package/dist/cli/context/session-buffer.d.ts +57 -0
  232. package/dist/cli/context/session-buffer.d.ts.map +1 -0
  233. package/dist/cli/context/session-buffer.js +268 -0
  234. package/dist/cli/context/session-buffer.js.map +1 -0
  235. package/dist/cli/context/trimmer.d.ts +26 -0
  236. package/dist/cli/context/trimmer.d.ts.map +1 -0
  237. package/dist/cli/context/trimmer.js +105 -0
  238. package/dist/cli/context/trimmer.js.map +1 -0
  239. package/dist/cli/feed/analyzer.d.ts +17 -0
  240. package/dist/cli/feed/analyzer.d.ts.map +1 -0
  241. package/dist/cli/feed/analyzer.js +220 -0
  242. package/dist/cli/feed/analyzer.js.map +1 -0
  243. package/dist/cli/feed/intent.d.ts +8 -0
  244. package/dist/cli/feed/intent.d.ts.map +1 -0
  245. package/dist/cli/feed/intent.js +70 -0
  246. package/dist/cli/feed/intent.js.map +1 -0
  247. package/dist/cli/feed/parser.d.ts +23 -0
  248. package/dist/cli/feed/parser.d.ts.map +1 -0
  249. package/dist/cli/feed/parser.js +166 -0
  250. package/dist/cli/feed/parser.js.map +1 -0
  251. package/dist/cli/feed/pipeline.d.ts +32 -0
  252. package/dist/cli/feed/pipeline.d.ts.map +1 -0
  253. package/dist/cli/feed/pipeline.js +242 -0
  254. package/dist/cli/feed/pipeline.js.map +1 -0
  255. package/dist/cli/feed/reader.d.ts +10 -0
  256. package/dist/cli/feed/reader.d.ts.map +1 -0
  257. package/dist/cli/feed/reader.js +61 -0
  258. package/dist/cli/feed/reader.js.map +1 -0
  259. package/dist/cli/feed/scanner.d.ts +10 -0
  260. package/dist/cli/feed/scanner.d.ts.map +1 -0
  261. package/dist/cli/feed/scanner.js +124 -0
  262. package/dist/cli/feed/scanner.js.map +1 -0
  263. package/dist/cli/feed/watcher.d.ts +14 -0
  264. package/dist/cli/feed/watcher.d.ts.map +1 -0
  265. package/dist/cli/feed/watcher.js +76 -0
  266. package/dist/cli/feed/watcher.js.map +1 -0
  267. package/dist/cli/index.d.ts +3 -0
  268. package/dist/cli/index.d.ts.map +1 -0
  269. package/dist/cli/index.js +204 -0
  270. package/dist/cli/index.js.map +1 -0
  271. package/dist/cli/providers/anthropic.d.ts +10 -0
  272. package/dist/cli/providers/anthropic.d.ts.map +1 -0
  273. package/dist/cli/providers/anthropic.js +117 -0
  274. package/dist/cli/providers/anthropic.js.map +1 -0
  275. package/dist/cli/providers/ollama.d.ts +37 -0
  276. package/dist/cli/providers/ollama.d.ts.map +1 -0
  277. package/dist/cli/providers/ollama.js +151 -0
  278. package/dist/cli/providers/ollama.js.map +1 -0
  279. package/dist/cli/providers/openai.d.ts +13 -0
  280. package/dist/cli/providers/openai.d.ts.map +1 -0
  281. package/dist/cli/providers/openai.js +283 -0
  282. package/dist/cli/providers/openai.js.map +1 -0
  283. package/dist/cli/providers/rate-limiter.d.ts +51 -0
  284. package/dist/cli/providers/rate-limiter.d.ts.map +1 -0
  285. package/dist/cli/providers/rate-limiter.js +164 -0
  286. package/dist/cli/providers/rate-limiter.js.map +1 -0
  287. package/dist/cli/providers/registry.d.ts +16 -0
  288. package/dist/cli/providers/registry.d.ts.map +1 -0
  289. package/dist/cli/providers/registry.js +99 -0
  290. package/dist/cli/providers/registry.js.map +1 -0
  291. package/dist/cli/providers/types.d.ts +61 -0
  292. package/dist/cli/providers/types.d.ts.map +1 -0
  293. package/dist/cli/providers/types.js +2 -0
  294. package/dist/cli/providers/types.js.map +1 -0
  295. package/dist/cli/sessions/manager.d.ts +69 -0
  296. package/dist/cli/sessions/manager.d.ts.map +1 -0
  297. package/dist/cli/sessions/manager.js +200 -0
  298. package/dist/cli/sessions/manager.js.map +1 -0
  299. package/dist/cli/sessions/session.d.ts +54 -0
  300. package/dist/cli/sessions/session.d.ts.map +1 -0
  301. package/dist/cli/sessions/session.js +70 -0
  302. package/dist/cli/sessions/session.js.map +1 -0
  303. package/dist/cli/sessions/tab-view.d.ts +18 -0
  304. package/dist/cli/sessions/tab-view.d.ts.map +1 -0
  305. package/dist/cli/sessions/tab-view.js +134 -0
  306. package/dist/cli/sessions/tab-view.js.map +1 -0
  307. package/dist/cli/ui/activity.d.ts +82 -0
  308. package/dist/cli/ui/activity.d.ts.map +1 -0
  309. package/dist/cli/ui/activity.js +309 -0
  310. package/dist/cli/ui/activity.js.map +1 -0
  311. package/dist/cli/ui/chat-view.d.ts +10 -0
  312. package/dist/cli/ui/chat-view.d.ts.map +1 -0
  313. package/dist/cli/ui/chat-view.js +165 -0
  314. package/dist/cli/ui/chat-view.js.map +1 -0
  315. package/dist/cli/ui/command-suggest.d.ts +22 -0
  316. package/dist/cli/ui/command-suggest.d.ts.map +1 -0
  317. package/dist/cli/ui/command-suggest.js +115 -0
  318. package/dist/cli/ui/command-suggest.js.map +1 -0
  319. package/dist/cli/ui/logo.d.ts +3 -0
  320. package/dist/cli/ui/logo.d.ts.map +1 -0
  321. package/dist/cli/ui/logo.js +25 -0
  322. package/dist/cli/ui/logo.js.map +1 -0
  323. package/dist/cli/ui/progress.d.ts +21 -0
  324. package/dist/cli/ui/progress.d.ts.map +1 -0
  325. package/dist/cli/ui/progress.js +125 -0
  326. package/dist/cli/ui/progress.js.map +1 -0
  327. package/dist/cli/ui/select-menu.d.ts +22 -0
  328. package/dist/cli/ui/select-menu.d.ts.map +1 -0
  329. package/dist/cli/ui/select-menu.js +152 -0
  330. package/dist/cli/ui/select-menu.js.map +1 -0
  331. package/dist/cli/ui/spinner.d.ts +3 -0
  332. package/dist/cli/ui/spinner.d.ts.map +1 -0
  333. package/dist/cli/ui/spinner.js +14 -0
  334. package/dist/cli/ui/spinner.js.map +1 -0
  335. package/dist/cli/ui/statusbar.d.ts +65 -0
  336. package/dist/cli/ui/statusbar.d.ts.map +1 -0
  337. package/dist/cli/ui/statusbar.js +272 -0
  338. package/dist/cli/ui/statusbar.js.map +1 -0
  339. package/dist/cli/ui/theme.d.ts +20 -0
  340. package/dist/cli/ui/theme.d.ts.map +1 -0
  341. package/dist/cli/ui/theme.js +25 -0
  342. package/dist/cli/ui/theme.js.map +1 -0
  343. package/dist/cli/ui/tool-output.d.ts +25 -0
  344. package/dist/cli/ui/tool-output.d.ts.map +1 -0
  345. package/dist/cli/ui/tool-output.js +171 -0
  346. package/dist/cli/ui/tool-output.js.map +1 -0
  347. package/dist/cli/validation/autofix.d.ts +32 -0
  348. package/dist/cli/validation/autofix.d.ts.map +1 -0
  349. package/dist/cli/validation/autofix.js +148 -0
  350. package/dist/cli/validation/autofix.js.map +1 -0
  351. package/dist/cli/validation/classifier.d.ts +17 -0
  352. package/dist/cli/validation/classifier.d.ts.map +1 -0
  353. package/dist/cli/validation/classifier.js +174 -0
  354. package/dist/cli/validation/classifier.js.map +1 -0
  355. package/dist/cli/validation/criteria.d.ts +22 -0
  356. package/dist/cli/validation/criteria.d.ts.map +1 -0
  357. package/dist/cli/validation/criteria.js +188 -0
  358. package/dist/cli/validation/criteria.js.map +1 -0
  359. package/dist/cli/validation/dynamic-checks.d.ts +16 -0
  360. package/dist/cli/validation/dynamic-checks.d.ts.map +1 -0
  361. package/dist/cli/validation/dynamic-checks.js +109 -0
  362. package/dist/cli/validation/dynamic-checks.js.map +1 -0
  363. package/dist/cli/validation/model.d.ts +16 -0
  364. package/dist/cli/validation/model.d.ts.map +1 -0
  365. package/dist/cli/validation/model.js +59 -0
  366. package/dist/cli/validation/model.js.map +1 -0
  367. package/dist/cli/validation/reporter.d.ts +18 -0
  368. package/dist/cli/validation/reporter.d.ts.map +1 -0
  369. package/dist/cli/validation/reporter.js +167 -0
  370. package/dist/cli/validation/reporter.js.map +1 -0
  371. package/dist/cli/validation/spiral-checks.d.ts +12 -0
  372. package/dist/cli/validation/spiral-checks.d.ts.map +1 -0
  373. package/dist/cli/validation/spiral-checks.js +167 -0
  374. package/dist/cli/validation/spiral-checks.js.map +1 -0
  375. package/dist/cli/validation/static-checks.d.ts +26 -0
  376. package/dist/cli/validation/static-checks.d.ts.map +1 -0
  377. package/dist/cli/validation/static-checks.js +492 -0
  378. package/dist/cli/validation/static-checks.js.map +1 -0
  379. package/dist/cli/validation/stats.d.ts +29 -0
  380. package/dist/cli/validation/stats.d.ts.map +1 -0
  381. package/dist/cli/validation/stats.js +137 -0
  382. package/dist/cli/validation/stats.js.map +1 -0
  383. package/dist/index.d.ts +3 -0
  384. package/dist/index.d.ts.map +1 -0
  385. package/dist/index.js +35 -0
  386. package/dist/index.js.map +1 -0
  387. package/dist/server.d.ts +8 -0
  388. package/dist/server.d.ts.map +1 -0
  389. package/dist/server.js +43 -0
  390. package/dist/server.js.map +1 -0
  391. package/dist/spiral/cloud/content-extractor.d.ts +27 -0
  392. package/dist/spiral/cloud/content-extractor.d.ts.map +1 -0
  393. package/dist/spiral/cloud/content-extractor.js +175 -0
  394. package/dist/spiral/cloud/content-extractor.js.map +1 -0
  395. package/dist/spiral/cloud/search-provider.d.ts +22 -0
  396. package/dist/spiral/cloud/search-provider.d.ts.map +1 -0
  397. package/dist/spiral/cloud/search-provider.js +212 -0
  398. package/dist/spiral/cloud/search-provider.js.map +1 -0
  399. package/dist/spiral/cloud/topic-detector.d.ts +25 -0
  400. package/dist/spiral/cloud/topic-detector.d.ts.map +1 -0
  401. package/dist/spiral/cloud/topic-detector.js +196 -0
  402. package/dist/spiral/cloud/topic-detector.js.map +1 -0
  403. package/dist/spiral/cloud/web-enricher.d.ts +58 -0
  404. package/dist/spiral/cloud/web-enricher.d.ts.map +1 -0
  405. package/dist/spiral/cloud/web-enricher.js +170 -0
  406. package/dist/spiral/cloud/web-enricher.js.map +1 -0
  407. package/dist/spiral/compression.d.ts +54 -0
  408. package/dist/spiral/compression.d.ts.map +1 -0
  409. package/dist/spiral/compression.js +175 -0
  410. package/dist/spiral/compression.js.map +1 -0
  411. package/dist/spiral/embeddings.d.ts +24 -0
  412. package/dist/spiral/embeddings.d.ts.map +1 -0
  413. package/dist/spiral/embeddings.js +69 -0
  414. package/dist/spiral/embeddings.js.map +1 -0
  415. package/dist/spiral/engine.d.ts +95 -0
  416. package/dist/spiral/engine.d.ts.map +1 -0
  417. package/dist/spiral/engine.js +271 -0
  418. package/dist/spiral/engine.js.map +1 -0
  419. package/dist/spiral/injection.d.ts +29 -0
  420. package/dist/spiral/injection.d.ts.map +1 -0
  421. package/dist/spiral/injection.js +164 -0
  422. package/dist/spiral/injection.js.map +1 -0
  423. package/dist/spiral/relevance.d.ts +37 -0
  424. package/dist/spiral/relevance.d.ts.map +1 -0
  425. package/dist/spiral/relevance.js +75 -0
  426. package/dist/spiral/relevance.js.map +1 -0
  427. package/dist/storage/database.d.ts +11 -0
  428. package/dist/storage/database.d.ts.map +1 -0
  429. package/dist/storage/database.js +141 -0
  430. package/dist/storage/database.js.map +1 -0
  431. package/dist/storage/edges.d.ts +25 -0
  432. package/dist/storage/edges.d.ts.map +1 -0
  433. package/dist/storage/edges.js +69 -0
  434. package/dist/storage/edges.js.map +1 -0
  435. package/dist/storage/nodes.d.ts +45 -0
  436. package/dist/storage/nodes.d.ts.map +1 -0
  437. package/dist/storage/nodes.js +124 -0
  438. package/dist/storage/nodes.js.map +1 -0
  439. package/dist/storage/vectors.d.ts +25 -0
  440. package/dist/storage/vectors.d.ts.map +1 -0
  441. package/dist/storage/vectors.js +110 -0
  442. package/dist/storage/vectors.js.map +1 -0
  443. package/dist/tools/spiral-compact.d.ts +14 -0
  444. package/dist/tools/spiral-compact.d.ts.map +1 -0
  445. package/dist/tools/spiral-compact.js +14 -0
  446. package/dist/tools/spiral-compact.js.map +1 -0
  447. package/dist/tools/spiral-query.d.ts +18 -0
  448. package/dist/tools/spiral-query.d.ts.map +1 -0
  449. package/dist/tools/spiral-query.js +16 -0
  450. package/dist/tools/spiral-query.js.map +1 -0
  451. package/dist/tools/spiral-relate.d.ts +21 -0
  452. package/dist/tools/spiral-relate.d.ts.map +1 -0
  453. package/dist/tools/spiral-relate.js +21 -0
  454. package/dist/tools/spiral-relate.js.map +1 -0
  455. package/dist/tools/spiral-status.d.ts +8 -0
  456. package/dist/tools/spiral-status.d.ts.map +1 -0
  457. package/dist/tools/spiral-status.js +10 -0
  458. package/dist/tools/spiral-status.js.map +1 -0
  459. package/dist/tools/spiral-store.d.ts +41 -0
  460. package/dist/tools/spiral-store.d.ts.map +1 -0
  461. package/dist/tools/spiral-store.js +22 -0
  462. package/dist/tools/spiral-store.js.map +1 -0
  463. package/dist/types.d.ts +92 -0
  464. package/dist/types.d.ts.map +1 -0
  465. package/dist/types.js +9 -0
  466. package/dist/types.js.map +1 -0
  467. package/dist/utils/config.d.ts +14 -0
  468. package/dist/utils/config.d.ts.map +1 -0
  469. package/dist/utils/config.js +44 -0
  470. package/dist/utils/config.js.map +1 -0
  471. package/dist/utils/logger.d.ts +10 -0
  472. package/dist/utils/logger.d.ts.map +1 -0
  473. package/dist/utils/logger.js +40 -0
  474. package/dist/utils/logger.js.map +1 -0
  475. package/dist/utils/tokens.d.ts +21 -0
  476. package/dist/utils/tokens.d.ts.map +1 -0
  477. package/dist/utils/tokens.js +33 -0
  478. package/dist/utils/tokens.js.map +1 -0
  479. package/package.json +90 -0
@@ -0,0 +1,2321 @@
1
+ import * as readline from 'node:readline';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { ConfigStore } from '../config/store.js';
5
+ import { createProvider } from '../providers/registry.js';
6
+ import { analyzeProject } from '../context/project.js';
7
+ import { assembleSystemPrompt } from '../context/assembler.js';
8
+ import { renderLogo } from '../ui/logo.js';
9
+ import { renderError, renderInfo, renderSpiralStatus, renderUserMessage, } from '../ui/chat-view.js';
10
+ import { isInsideToolBlock } from '../ui/tool-output.js';
11
+ import { renderFeedProgress, renderFeedSummary } from '../ui/progress.js';
12
+ import { ActivityIndicator } from '../ui/activity.js';
13
+ import { theme } from '../ui/theme.js';
14
+ import { detectFeedIntent } from '../feed/intent.js';
15
+ import { runFeedPipeline } from '../feed/pipeline.js';
16
+ import { showHelixMenu } from './helix-menu.js';
17
+ import { initializeTools } from '../agent/tools/registry.js';
18
+ import { runAgentLoop, AgentController, AgentAbortError } from '../agent/loop.js';
19
+ import { PermissionManager } from '../agent/permissions.js';
20
+ import { UndoStack } from '../agent/undo.js';
21
+ import { writeStatusBar, renderStatusBar, getGitInfo, truncateBar } from '../ui/statusbar.js';
22
+ import { CheckpointStore } from '../checkpoints/store.js';
23
+ import { createKeybindingState, processKeypress } from '../checkpoints/keybinding.js';
24
+ import { runCheckpointBrowser } from '../checkpoints/browser.js';
25
+ import { runFirstTimeSetup, showModelSwitcher, showKeyManagement } from './setup.js';
26
+ import { SessionBuffer } from '../context/session-buffer.js';
27
+ import { trimConversation } from '../context/trimmer.js';
28
+ import { runAutonomousLoop, SECURITY_PROMPT } from '../agent/autonomous.js';
29
+ import { SessionManager } from '../sessions/manager.js';
30
+ import { renderSessionNotification, renderSessionList } from '../sessions/tab-view.js';
31
+ import { getSuggestions, writeSuggestions, clearSuggestions } from '../ui/command-suggest.js';
32
+ import { selectMenu } from '../ui/select-menu.js';
33
+ import { classifyTask } from '../validation/classifier.js';
34
+ import { generateCriteria } from '../validation/criteria.js';
35
+ import { validationLoop } from '../validation/autofix.js';
36
+ import { createValidationProvider } from '../validation/model.js';
37
+ import { renderValidationSummary, renderValidationStart, renderClassification } from '../validation/reporter.js';
38
+ import { storeValidationResult, getValidationStats, renderValidationStats } from '../validation/stats.js';
39
+ import chalk from 'chalk';
40
+ const HELP_CATEGORIES = [
41
+ {
42
+ category: 'Chat & Interaction', color: '#00d4ff',
43
+ items: [
44
+ { cmd: '/clear', label: '/clear', description: 'Clear conversation history' },
45
+ { cmd: '/model', label: '/model', description: 'Switch LLM model' },
46
+ { cmd: '/keys', label: '/keys', description: 'Manage API keys' },
47
+ { cmd: '/yolo', label: '/yolo', description: 'Toggle YOLO mode' },
48
+ { cmd: '/skip-permissions', label: '/skip-permissions', description: 'Toggle skip-permissions' },
49
+ ],
50
+ },
51
+ {
52
+ category: 'Spiral Memory', color: '#00ff88',
53
+ items: [
54
+ { cmd: '/spiral', label: '/spiral', description: 'Show spiral status (nodes per level)' },
55
+ { cmd: '/feed', label: '/feed', description: 'Feed files into spiral' },
56
+ { cmd: '/context', label: '/context', description: 'Show context size & embeddings' },
57
+ { cmd: '/compact', label: '/compact', description: 'Trigger spiral evolution' },
58
+ { cmd: '/tokens', label: '/tokens', description: 'Show token usage & memory' },
59
+ ],
60
+ },
61
+ {
62
+ category: 'Visualization & Brain', color: '#4169e1',
63
+ items: [
64
+ { cmd: '/brain', label: '/brain', description: 'Brain scope + 3D visualization' },
65
+ { cmd: '/brain local', label: '/brain local', description: 'Switch to project-local brain' },
66
+ { cmd: '/brain global', label: '/brain global', description: 'Switch to global brain' },
67
+ { cmd: '/helix', label: '/helix', description: 'Command Center + Brain (auto-start)' },
68
+ { cmd: '/helixlocal', label: '/helixlocal', description: 'Command Center + local brain' },
69
+ { cmd: '/helixglobal', label: '/helixglobal', description: 'Command Center + global brain' },
70
+ ],
71
+ },
72
+ {
73
+ category: 'Autonomous & Security', color: '#ff6600',
74
+ items: [
75
+ { cmd: '/auto', label: '/auto', description: 'Autonomous mode' },
76
+ { cmd: '/stop', label: '/stop', description: 'Stop autonomous mode' },
77
+ { cmd: '/security', label: '/security', description: 'Run security audit (background)' },
78
+ { cmd: '/sessions', label: '/sessions', description: 'List all sessions & tabs' },
79
+ { cmd: '/local', label: '/local', description: 'Local LLM setup (Ollama)' },
80
+ ],
81
+ },
82
+ {
83
+ category: 'Validation Matrix', color: '#00cc66',
84
+ items: [
85
+ { cmd: '/validation', label: '/validation', description: 'Show validation status' },
86
+ { cmd: '/validation on', label: '/validation on', description: 'Enable output validation' },
87
+ { cmd: '/validation off', label: '/validation off', description: 'Disable output validation' },
88
+ { cmd: '/validation verbose', label: '/validation verbose', description: 'Toggle verbose mode' },
89
+ { cmd: '/validation strict', label: '/validation strict', description: 'Toggle strict mode' },
90
+ { cmd: '/validation stats', label: '/validation stats', description: 'Show validation statistics' },
91
+ ],
92
+ },
93
+ {
94
+ category: 'Code & Git', color: '#8a2be2',
95
+ items: [
96
+ { cmd: '/undo', label: '/undo', description: 'Undo file changes' },
97
+ { cmd: '/diff', label: '/diff', description: 'Show uncommitted git changes' },
98
+ { cmd: '/git', label: '/git', description: 'Show git branch & status' },
99
+ { cmd: '/project', label: '/project', description: 'Show project info' },
100
+ { cmd: '/export', label: '/export', description: 'Export spiral as ZIP' },
101
+ ],
102
+ },
103
+ {
104
+ category: 'Account & Auth', color: '#00d4ff',
105
+ items: [
106
+ { cmd: '/login', label: '/login', description: 'Log in to HelixMind web platform' },
107
+ { cmd: '/logout', label: '/logout', description: 'Log out and revoke API key' },
108
+ { cmd: '/whoami', label: '/whoami', description: 'Show account & plan info' },
109
+ ],
110
+ },
111
+ {
112
+ category: 'Navigation', color: '#6c757d',
113
+ items: [
114
+ { cmd: '/exit', label: '/exit', description: 'Exit HelixMind' },
115
+ ],
116
+ },
117
+ ];
118
+ /** Build flat MenuItem[] with category headers as disabled separators */
119
+ function buildHelpMenuItems() {
120
+ const items = [];
121
+ const commands = [];
122
+ for (const cat of HELP_CATEGORIES) {
123
+ items.push({ label: chalk.hex(cat.color).bold(cat.category), disabled: true });
124
+ commands.push('');
125
+ for (const item of cat.items) {
126
+ items.push({ label: theme.primary(item.label), description: item.description });
127
+ commands.push(item.cmd);
128
+ }
129
+ }
130
+ items.push({ label: '', disabled: true });
131
+ commands.push('');
132
+ items.push({ label: chalk.dim('ESC Stop Agent | Ctrl+C Clear | Tab Autocomplete'), disabled: true });
133
+ commands.push('');
134
+ return { items, commands };
135
+ }
136
+ // Keep static HELP_TEXT as fallback for non-TTY
137
+ const HELP_TEXT = `
138
+ ${chalk.hex('#00d4ff').bold(' Chat & Interaction')}
139
+ ${theme.primary('/help'.padEnd(22))} ${theme.dim('Show this help')}
140
+ ${theme.primary('/clear'.padEnd(22))} ${theme.dim('Clear conversation history')}
141
+ ${theme.primary('/model [name]'.padEnd(22))} ${theme.dim('Switch model (interactive or direct: /model gpt-4o)')}
142
+ ${theme.primary('/keys'.padEnd(22))} ${theme.dim('Add/remove/update API keys')}
143
+ ${theme.primary('/yolo [on|off]'.padEnd(22))} ${theme.dim('Toggle YOLO mode — auto-approve ALL operations')}
144
+ ${theme.primary('/skip-permissions'.padEnd(22))} ${theme.dim('Toggle skip-permissions (auto-approve safe ops)')}
145
+
146
+ ${chalk.hex('#00ff88').bold(' Spiral Memory')}
147
+ ${theme.primary('/spiral'.padEnd(22))} ${theme.dim('Show spiral status (nodes per level)')}
148
+ ${theme.primary('/feed [path]'.padEnd(22))} ${theme.dim('Feed files into spiral (default: current dir)')}
149
+ ${theme.primary('/context'.padEnd(22))} ${theme.dim('Show current context size & embeddings')}
150
+ ${theme.primary('/compact'.padEnd(22))} ${theme.dim('Trigger spiral evolution (promote/demote nodes)')}
151
+ ${theme.primary('/tokens'.padEnd(22))} ${theme.dim('Show token usage, checkpoints, memory')}
152
+
153
+ ${chalk.hex('#4169e1').bold(' Visualization & Brain')}
154
+ ${theme.primary('/brain'.padEnd(22))} ${theme.dim('Show brain scope + open 3D visualization')}
155
+ ${theme.primary('/brain local'.padEnd(22))} ${theme.dim('Switch to project-local brain (.helixmind/)')}
156
+ ${theme.primary('/brain global'.padEnd(22))} ${theme.dim('Switch to global brain (~/.spiral-context/)')}
157
+ ${theme.primary('/helix'.padEnd(22))} ${theme.dim('Command Center + Brain (auto-start local)')}
158
+ ${theme.primary('/helixlocal'.padEnd(22))} ${theme.dim('Command Center + local brain')}
159
+ ${theme.primary('/helixglobal'.padEnd(22))} ${theme.dim('Command Center + global brain')}
160
+
161
+ ${chalk.hex('#ff6600').bold(' Autonomous & Security')}
162
+ ${theme.primary('/auto'.padEnd(22))} ${theme.dim('Autonomous mode \u2014 find & fix issues continuously')}
163
+ ${theme.primary('/stop'.padEnd(22))} ${theme.dim('Stop autonomous mode')}
164
+ ${theme.primary('/security'.padEnd(22))} ${theme.dim('Run comprehensive security audit (background)')}
165
+ ${theme.primary('/sessions'.padEnd(22))} ${theme.dim('List all sessions & tabs')}
166
+ ${theme.primary('/local'.padEnd(22))} ${theme.dim('Local LLM setup \u2014 Ollama models')}
167
+
168
+ ${chalk.hex('#00cc66').bold(' Validation Matrix')}
169
+ ${theme.primary('/validation'.padEnd(22))} ${theme.dim('Show validation mode')}
170
+ ${theme.primary('/validation on'.padEnd(22))} ${theme.dim('Enable output validation')}
171
+ ${theme.primary('/validation off'.padEnd(22))} ${theme.dim('Disable output validation')}
172
+ ${theme.primary('/validation verbose'.padEnd(22))} ${theme.dim('Show every check detail')}
173
+ ${theme.primary('/validation strict'.padEnd(22))} ${theme.dim('Treat warnings as errors')}
174
+ ${theme.primary('/validation stats'.padEnd(22))} ${theme.dim('Show validation statistics')}
175
+
176
+ ${chalk.hex('#8a2be2').bold(' Code & Git')}
177
+ ${theme.primary('/undo [n|list]'.padEnd(22))} ${theme.dim('Undo last n file changes (or list history)')}
178
+ ${theme.primary('/diff'.padEnd(22))} ${theme.dim('Show all uncommitted git changes')}
179
+ ${theme.primary('/git'.padEnd(22))} ${theme.dim('Show git branch & status')}
180
+ ${theme.primary('/project'.padEnd(22))} ${theme.dim('Show detected project info')}
181
+ ${theme.primary('/export [dir]'.padEnd(22))} ${theme.dim('Export spiral as ZIP archive')}
182
+
183
+ ${chalk.hex('#6c757d').bold(' Navigation')}
184
+ ${theme.primary('/exit /quit'.padEnd(22))} ${theme.dim('Exit HelixMind')}
185
+ ${theme.dim(' ESC'.padEnd(22))} ${theme.dim('Stop agent (immediately interrupts)')}
186
+ ${theme.dim(' Ctrl+C'.padEnd(22))} ${theme.dim('Clear input (or double to force exit)')}
187
+ ${theme.dim(' Tab'.padEnd(22))} ${theme.dim('Autocomplete command')}
188
+ `;
189
+ export async function chatCommand(options) {
190
+ const configDir = join(homedir(), '.helixmind');
191
+ const store = new ConfigStore(configDir);
192
+ let config = store.getAll();
193
+ // Show logo early
194
+ process.stdout.write(renderLogo());
195
+ // ─── Auth Gate: require login on first use ───────────────────
196
+ // Once logged in, credentials are cached locally.
197
+ // Offline use works with cached auth — no server needed.
198
+ if (!store.isLoggedIn()) {
199
+ const { requireAuth } = await import('../auth/guard.js');
200
+ await requireAuth();
201
+ config = store.getAll();
202
+ }
203
+ else {
204
+ // Background auth check: verify token is still valid when online.
205
+ // If offline or server unreachable, cached auth stays valid silently.
206
+ import('../auth/feature-gate.js').then(({ refreshPlanInfo }) => refreshPlanInfo(store)).catch(() => { });
207
+ }
208
+ // First-time setup: prompt for LLM API key if none configured
209
+ if (!store.hasApiKey()) {
210
+ const success = await runFirstTimeSetup(store);
211
+ if (!success) {
212
+ process.exit(1);
213
+ }
214
+ config = store.getAll();
215
+ }
216
+ // Create provider
217
+ let provider;
218
+ try {
219
+ provider = createProvider(config.provider, config.apiKey, config.model, config.providers[config.provider]?.baseURL);
220
+ }
221
+ catch (err) {
222
+ renderError(`Failed to initialize provider: ${err}`);
223
+ process.exit(1);
224
+ }
225
+ // Register rate limit handler for user-visible feedback
226
+ const { onRateLimitWait } = await import('../providers/rate-limiter.js');
227
+ onRateLimitWait((waitMs, reason) => {
228
+ if (waitMs > 1000) {
229
+ process.stdout.write(`\r\x1b[K ${chalk.yellow('\u23F3')} ${chalk.dim(`Rate limit: waiting ${Math.ceil(waitMs / 1000)}s (${reason})`)}`);
230
+ }
231
+ });
232
+ // Initialize agent tools
233
+ await initializeTools();
234
+ // Analyze project context
235
+ const project = await analyzeProject(process.cwd());
236
+ // Conversation history (for agent loop, we use ToolMessage format)
237
+ const messages = [];
238
+ const agentHistory = [];
239
+ // Permission manager
240
+ const permissions = new PermissionManager();
241
+ if (options.yolo)
242
+ permissions.setYolo(true);
243
+ if (options.skipPermissions)
244
+ permissions.setSkipPermissions(true);
245
+ // Undo stack
246
+ const undoStack = new UndoStack();
247
+ // Checkpoint store
248
+ const checkpointStore = new CheckpointStore();
249
+ // Session buffer (working memory)
250
+ const sessionBuffer = new SessionBuffer();
251
+ // Activity indicator (replaces spinner)
252
+ const activity = new ActivityIndicator();
253
+ // Agent controller for pause/resume
254
+ const agentController = new AgentController();
255
+ let agentRunning = false;
256
+ let autonomousMode = false;
257
+ // Forward-declared findings handler (reassigned by control protocol if active)
258
+ let pushFindingsToBrainFn = null;
259
+ // Session Manager — manages background sessions (security, auto, etc.)
260
+ const sessionMgr = new SessionManager({
261
+ flags: {
262
+ yolo: options.yolo ?? false,
263
+ skipPermissions: options.skipPermissions ?? false,
264
+ },
265
+ onSessionComplete: (session) => {
266
+ // Show notification in the terminal when a background session finishes
267
+ if (session.id !== 'main') {
268
+ process.stdout.write(renderSessionNotification(session));
269
+ // Push findings to brain visualization (if browser is open)
270
+ if (session.result?.text && pushFindingsToBrainFn) {
271
+ pushFindingsToBrainFn(session);
272
+ }
273
+ updateStatusBar();
274
+ // Re-prompt if user is idle — use showPrompt() for full separator+hint+statusbar
275
+ if (!agentRunning) {
276
+ showPrompt();
277
+ }
278
+ }
279
+ },
280
+ onSessionAutoClose: () => {
281
+ // Tab was auto-removed after timeout — refresh the tab bar
282
+ updateStatusBar();
283
+ },
284
+ });
285
+ // Validation Matrix state
286
+ let validationEnabled = options.validation !== false; // Default ON
287
+ let validationVerbose = options.validationVerbose ?? false;
288
+ let validationStrict = options.validationStrict ?? false;
289
+ // Session metrics
290
+ let sessionTokensInput = 0;
291
+ let sessionTokensOutput = 0;
292
+ let sessionToolCalls = 0;
293
+ let roundToolCalls = 0;
294
+ // Brain scope: project-local if .helixmind/ exists, else global
295
+ // Auto-create .helixmind/ for new projects (opt-in for local brain)
296
+ const { detectBrainScope, resolveDataDir: resolveSpiralDir, loadConfig: loadSpiralConfig } = await import('../../utils/config.js');
297
+ const { mkdirSync, existsSync } = await import('node:fs');
298
+ let brainScope = detectBrainScope(process.cwd());
299
+ // Auto-create .helixmind/ if it doesn't exist (local brain by default for projects)
300
+ const helixDir = join(process.cwd(), '.helixmind');
301
+ if (!existsSync(helixDir)) {
302
+ mkdirSync(helixDir, { recursive: true });
303
+ renderInfo(chalk.dim(' Created .helixmind/ directory for local brain'));
304
+ brainScope = 'project';
305
+ }
306
+ let spiralEngine = null;
307
+ async function initSpiralEngine(scope) {
308
+ try {
309
+ const { SpiralEngine } = await import('../../spiral/engine.js');
310
+ const dataDir = resolveSpiralDir(scope, process.cwd());
311
+ const spiralConfig = loadSpiralConfig(dataDir);
312
+ const engine = new SpiralEngine(spiralConfig);
313
+ await engine.initialize();
314
+ return engine;
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ if (config.spiral.enabled) {
321
+ spiralEngine = await initSpiralEngine(brainScope);
322
+ }
323
+ // Create session start checkpoint
324
+ checkpointStore.create({
325
+ type: 'session_start',
326
+ label: 'Session started',
327
+ messageIndex: 0,
328
+ });
329
+ // Single message mode
330
+ if (options.message) {
331
+ await sendAgentMessage(options.message, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
332
+ spiralEngine?.close();
333
+ return;
334
+ }
335
+ // Interactive mode
336
+ renderInfo(` Provider: ${config.provider} | Model: ${config.model}`);
337
+ if (project.name !== 'unknown') {
338
+ renderInfo(` Project: ${project.name} (${project.type})`);
339
+ }
340
+ const brainLabel = brainScope === 'project'
341
+ ? chalk.cyan('project-local') + chalk.dim(` (.helixmind/)`)
342
+ : chalk.dim('global') + chalk.dim(` (~/.spiral-context/)`);
343
+ renderInfo(` Brain: ${brainLabel}`);
344
+ // Show mode-specific startup info
345
+ const modeLabel = permissions.getModeLabel();
346
+ renderInfo(` Agent mode: ${modeLabel} permissions`);
347
+ // Show warnings for skip-permissions / yolo
348
+ if (options.skipPermissions && options.yolo) {
349
+ showFullAutonomousWarning();
350
+ }
351
+ else if (options.skipPermissions) {
352
+ showSkipPermissionsWarning();
353
+ }
354
+ // === Start Brain Server BEFORE prompt (no async output during typing) ===
355
+ let brainUrl = null;
356
+ if (spiralEngine && config.spiral.enabled) {
357
+ try {
358
+ const { exportBrainData } = await import('../brain/exporter.js');
359
+ const { startLiveBrain } = await import('../brain/generator.js');
360
+ const data = exportBrainData(spiralEngine, project.name || 'HelixMind', brainScope);
361
+ if (data.meta.totalNodes > 0) {
362
+ brainUrl = await startLiveBrain(spiralEngine, project.name || 'HelixMind', brainScope);
363
+ renderInfo(` \u{1F9E0} Brain: ${chalk.dim(brainUrl)}`);
364
+ }
365
+ }
366
+ catch { /* brain server optional */ }
367
+ }
368
+ // === Register CLI ↔ Web control protocol ===
369
+ if (brainUrl) {
370
+ try {
371
+ const { registerControlHandlers, setInstanceMeta, getBrainToken, pushSessionCreated, pushSessionUpdate, pushSessionRemoved, pushOutputLine, startRelayClient, } = await import('../brain/generator.js');
372
+ const { serializeSession, buildInstanceMeta, resetInstanceStartTime } = await import('../brain/control-protocol.js');
373
+ resetInstanceStartTime();
374
+ // Collected findings for getFindings() handler
375
+ const collectedFindings = [];
376
+ // Set instance metadata for discovery
377
+ const instanceId = (await import('node:crypto')).randomUUID().slice(0, 8);
378
+ const updateMeta = () => {
379
+ const meta = buildInstanceMeta(project.name || 'HelixMind', process.cwd(), config.model, config.provider, '0.1.0', instanceId);
380
+ setInstanceMeta(meta);
381
+ return meta;
382
+ };
383
+ updateMeta();
384
+ // Wire output streaming: when any session captures output, push to control clients
385
+ const wireSessionOutput = (session) => {
386
+ session.onCapture = (line, index) => {
387
+ pushOutputLine(session.id, line, index);
388
+ };
389
+ };
390
+ // Register control handlers
391
+ registerControlHandlers({
392
+ listSessions: () => sessionMgr.all.map(serializeSession),
393
+ startAuto: (goal) => {
394
+ const sessionName = goal ? `\u{1F504} Auto: ${goal.slice(0, 30)}` : '\u{1F504} Auto';
395
+ const bgSession = sessionMgr.create(sessionName, '\u{1F504}', agentHistory);
396
+ bgSession.start();
397
+ wireSessionOutput(bgSession);
398
+ pushSessionCreated(serializeSession(bgSession));
399
+ // Trigger autonomous mode (same as /auto start)
400
+ autonomousMode = true;
401
+ (async () => {
402
+ const completed = [];
403
+ try {
404
+ await runAutonomousLoop({
405
+ sendMessage: async (prompt) => {
406
+ bgSession.controller.reset();
407
+ const resultTextHolder = { text: '' };
408
+ const origAddSummary = bgSession.buffer.addAssistantSummary.bind(bgSession.buffer);
409
+ bgSession.buffer.addAssistantSummary = (t) => {
410
+ resultTextHolder.text = t;
411
+ origAddSummary(t);
412
+ };
413
+ await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
414
+ bgSession.buffer.addAssistantSummary = origAddSummary;
415
+ return resultTextHolder.text;
416
+ },
417
+ isAborted: () => !autonomousMode || bgSession.controller.isAborted,
418
+ onRoundStart: (round) => {
419
+ bgSession.controller.reset();
420
+ bgSession.capture(`Round ${round}...`);
421
+ },
422
+ onRoundEnd: (_round, summary) => {
423
+ completed.push(summary);
424
+ bgSession.capture(`\u2713 ${summary}`);
425
+ pushSessionUpdate(serializeSession(bgSession));
426
+ },
427
+ updateStatus: () => updateStatusBar(),
428
+ }, goal);
429
+ }
430
+ catch (err) {
431
+ if (!(err instanceof AgentAbortError)) {
432
+ bgSession.capture(`Error: ${err}`);
433
+ }
434
+ }
435
+ autonomousMode = false;
436
+ sessionMgr.complete(bgSession.id, {
437
+ text: completed.join('\n'),
438
+ steps: [],
439
+ errors: bgSession.controller.isAborted ? ['Aborted by user'] : [],
440
+ durationMs: bgSession.elapsed,
441
+ });
442
+ pushSessionUpdate(serializeSession(bgSession));
443
+ })();
444
+ return bgSession.id;
445
+ },
446
+ startSecurity: () => {
447
+ const bgSession = sessionMgr.create('\u{1F512} Security', '\u{1F512}', agentHistory);
448
+ bgSession.start();
449
+ wireSessionOutput(bgSession);
450
+ pushSessionCreated(serializeSession(bgSession));
451
+ runBackgroundSession(bgSession, SECURITY_PROMPT, provider, project, spiralEngine, config, permissions, checkpointStore, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }).then(result => {
452
+ sessionMgr.complete(bgSession.id, result);
453
+ pushSessionUpdate(serializeSession(bgSession));
454
+ }).catch(err => {
455
+ if (!(err instanceof AgentAbortError)) {
456
+ sessionMgr.complete(bgSession.id, {
457
+ text: '',
458
+ steps: [],
459
+ errors: [err instanceof Error ? err.message : String(err)],
460
+ durationMs: bgSession.elapsed,
461
+ });
462
+ pushSessionUpdate(serializeSession(bgSession));
463
+ }
464
+ });
465
+ return bgSession.id;
466
+ },
467
+ abortSession: (sessionId) => {
468
+ const session = sessionMgr.get(sessionId);
469
+ if (!session)
470
+ return false;
471
+ sessionMgr.abort(sessionId);
472
+ pushSessionUpdate(serializeSession(session));
473
+ return true;
474
+ },
475
+ sendChat: (text) => {
476
+ // Queue chat text to be processed as if the user typed it
477
+ typeAheadBuffer.push(text);
478
+ },
479
+ getFindings: () => [...collectedFindings],
480
+ });
481
+ // Override forward-reference to also collect findings for control protocol
482
+ pushFindingsToBrainFn = (session) => {
483
+ pushFindingsToBrain(session);
484
+ // Also collect findings for the control protocol
485
+ const text = session.result?.text || '';
486
+ const severityPatterns = [
487
+ { regex: /\*\*CRITICAL\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'critical' },
488
+ { regex: /\*\*HIGH\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'high' },
489
+ { regex: /\*\*MEDIUM\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'medium' },
490
+ { regex: /\*\*LOW\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'low' },
491
+ { regex: /DONE:\s*(.+?)(?:\n|$)/gi, severity: 'info' },
492
+ ];
493
+ for (const { regex, severity } of severityPatterns) {
494
+ let match;
495
+ while ((match = regex.exec(text)) !== null) {
496
+ const finding = match[1].trim();
497
+ if (finding.length > 5) {
498
+ const fileMatch = finding.match(/(?:in |file[:\s]+|path[:\s]+)([^\s,]+\.\w+)/i);
499
+ collectedFindings.push({
500
+ sessionName: session.name,
501
+ finding,
502
+ severity,
503
+ file: fileMatch?.[1] || '',
504
+ timestamp: Date.now(),
505
+ });
506
+ }
507
+ }
508
+ }
509
+ };
510
+ // Log connection token
511
+ const token = getBrainToken();
512
+ if (token) {
513
+ renderInfo(` \u{1F511} Brain token: ${chalk.dim(token.slice(-4))} ${chalk.dim('(full token in /brain)')}`);
514
+ }
515
+ // Start relay client if configured
516
+ const relayUrl = config.relay?.url;
517
+ const relayApiKey = config.relay?.apiKey;
518
+ if (relayUrl && relayApiKey) {
519
+ startRelayClient(relayUrl, relayApiKey, {
520
+ listSessions: () => sessionMgr.all.map(serializeSession),
521
+ startAuto: (goal) => { /* relay delegates to local handlers — already registered */ return ''; },
522
+ startSecurity: () => '',
523
+ abortSession: (id) => { sessionMgr.abort(id); return true; },
524
+ sendChat: (text) => { typeAheadBuffer.push(text); },
525
+ getFindings: () => [...collectedFindings],
526
+ }, updateMeta).catch(() => { });
527
+ }
528
+ }
529
+ catch { /* control protocol optional */ }
530
+ }
531
+ renderInfo(` Type /help for commands, ESC = stop agent, Ctrl+C twice to exit\n`);
532
+ process.stdout.write(theme.separator + '\n');
533
+ // Flag: true while user is at the prompt (typing). Footer timer skips updates
534
+ // when this is true to prevent cursor-jumping from interfering with readline.
535
+ let isAtPrompt = false;
536
+ // Flag: true while inline progress (\r\x1b[K) is actively writing.
537
+ // Suppresses the footer timer to prevent cursor-jumping flicker.
538
+ let inlineProgressActive = false;
539
+ // Wrap renderFeedProgress to automatically suppress footer timer during feed
540
+ const wrappedFeedProgress = (progress) => {
541
+ inlineProgressActive = progress.stage !== 'done';
542
+ renderFeedProgress(progress);
543
+ };
544
+ // Guard: ignore line events shortly after a sub-menu that used its own readline
545
+ // (e.g. /model → Add provider → askText). The sub-readline can leave phantom
546
+ // line events on stdin that would be misinterpreted as user messages.
547
+ let drainUntil = 0;
548
+ /**
549
+ * Build the readline prompt string. Must be a SINGLE line with ANSI codes
550
+ * wrapped in \x01..\x02 so readline can correctly compute visible width
551
+ * and position the cursor where the user types.
552
+ */
553
+ function makePrompt() {
554
+ const ansiStart = '\x01'; // RL_PROMPT_START_IGNORE
555
+ const ansiEnd = '\x02'; // RL_PROMPT_END_IGNORE
556
+ // Wrap each ANSI escape sequence so readline ignores it for width calculation
557
+ const gt = chalk.hex('#00d4ff').bold('>');
558
+ const escaped = gt.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
559
+ return `${escaped} `;
560
+ }
561
+ /**
562
+ * Show the full prompt area:
563
+ * ──────────────────
564
+ * ▸▸ safe permissions · esc = stop · /help
565
+ * 🌀 L1:... | tokens | model | git
566
+ * > _ ← cursor here (last line)
567
+ *
568
+ * Info is written ABOVE the prompt as normal scrolling text.
569
+ * The prompt is always the last line — no ANSI cursor tricks needed.
570
+ */
571
+ function showPrompt() {
572
+ const w = Math.max(20, (process.stdout.columns || 80) - 2);
573
+ const sep = chalk.hex('#00d4ff').dim('\u2500'.repeat(w));
574
+ const data = getStatusBarData();
575
+ const bar = renderStatusBar(data, w);
576
+ // Build hint line
577
+ const hints = [];
578
+ if (data.permissionMode === 'yolo')
579
+ hints.push(chalk.red('\u25B8\u25B8 yolo mode'));
580
+ else if (data.permissionMode === 'skip')
581
+ hints.push(chalk.yellow('\u25B8\u25B8 skip permissions'));
582
+ else
583
+ hints.push(chalk.green('\u25B8\u25B8 safe permissions'));
584
+ hints.push(chalk.dim('esc = stop'));
585
+ hints.push(chalk.dim('/help'));
586
+ const hintLine = hints.join(chalk.dim(' \u00B7 '));
587
+ // Write info above the prompt, then the prompt as the last line
588
+ isAtPrompt = true;
589
+ process.stdout.write(`\n${sep}\n ${hintLine}\n ${bar}\n`);
590
+ rl.prompt();
591
+ }
592
+ /** Build current status bar data object */
593
+ function getStatusBarData() {
594
+ const spiralStatus = spiralEngine ? spiralEngine.status() : null;
595
+ const git = getGitInfo(process.cwd());
596
+ const l6Count = spiralEngine ? spiralEngine.webKnowledgeCount() : 0;
597
+ return {
598
+ spiral: spiralStatus ? {
599
+ l1: spiralStatus.per_level[1] ?? 0,
600
+ l2: spiralStatus.per_level[2] ?? 0,
601
+ l3: spiralStatus.per_level[3] ?? 0,
602
+ l4: spiralStatus.per_level[4] ?? 0,
603
+ l5: spiralStatus.per_level[5] ?? 0,
604
+ l6: l6Count,
605
+ } : { l1: 0, l2: 0, l3: 0, l4: 0, l5: 0, l6: 0 },
606
+ sessionTokens: sessionTokensInput + sessionTokensOutput,
607
+ tokens: {
608
+ thisMessage: sessionTokensOutput,
609
+ thisSession: sessionTokensInput + sessionTokensOutput,
610
+ },
611
+ tools: { callsThisRound: roundToolCalls },
612
+ model: config.model,
613
+ git,
614
+ checkpoints: checkpointStore.count,
615
+ permissionMode: permissions.getModeLabel(),
616
+ autonomous: autonomousMode,
617
+ paused: agentController.isPaused,
618
+ plan: store.get('relay.plan') ?? undefined,
619
+ };
620
+ }
621
+ const rl = readline.createInterface({
622
+ input: process.stdin,
623
+ output: process.stdout,
624
+ prompt: makePrompt(),
625
+ completer: (line) => {
626
+ if (line.startsWith('/')) {
627
+ const matches = getSuggestions(line).map(s => s.cmd);
628
+ return [matches.length > 0 ? matches : [], line];
629
+ }
630
+ return [[], line];
631
+ },
632
+ });
633
+ // Update prompt and activity scroll region on terminal resize
634
+ process.stdout.on('resize', () => {
635
+ rl.setPrompt(makePrompt());
636
+ activity.handleResize();
637
+ });
638
+ // Track suggestion overlay state
639
+ let lastSuggestionCount = 0;
640
+ permissions.setReadline(rl);
641
+ permissions.setPromptCallback((active) => { isAtPrompt = active; });
642
+ // Activity indicator renders on the bottom terminal row (absolute positioned,
643
+ // same row as statusbar). The footer timer already skips statusbar draws when
644
+ // activity.isAnimating is true, so there's no conflict.
645
+ // Ctrl+C behavior:
646
+ // - If there's text on the line → clear the line (like a normal terminal)
647
+ // - If line is empty → count towards exit (double Ctrl+C = exit)
648
+ let ctrlCCount = 0;
649
+ let ctrlCTimer = null;
650
+ process.on('SIGINT', () => {
651
+ // If agent is running, treat Ctrl+C as interrupt
652
+ if (agentRunning) {
653
+ activity.stop('Stopped');
654
+ agentController.abort();
655
+ autonomousMode = false;
656
+ process.stdout.write('\n');
657
+ renderInfo('\u23F9 Agent interrupted.');
658
+ return;
659
+ }
660
+ // Check if readline has text on the current line
661
+ const currentLine = rl.line || '';
662
+ if (currentLine.length > 0) {
663
+ // Clear current input — write a new line and re-prompt
664
+ process.stdout.write('\n');
665
+ rl.line = '';
666
+ rl.cursor = 0;
667
+ isAtPrompt = true;
668
+ rl.prompt();
669
+ ctrlCCount = 0;
670
+ return;
671
+ }
672
+ // Empty line — count towards exit
673
+ ctrlCCount++;
674
+ if (ctrlCCount >= 2) {
675
+ process.stdout.write('\n');
676
+ renderInfo('Force exit \u2014 saving state...');
677
+ if (spiralEngine) {
678
+ try {
679
+ spiralEngine.saveState(messages).catch(() => { });
680
+ }
681
+ catch { /* best effort */ }
682
+ spiralEngine.close();
683
+ }
684
+ // Stop brain server
685
+ import('../brain/generator.js').then(m => m.stopLiveBrain()).catch(() => { });
686
+ process.exit(0);
687
+ }
688
+ process.stdout.write('\n');
689
+ renderInfo('Press Ctrl+C again to exit, or type a message to continue.');
690
+ isAtPrompt = true;
691
+ rl.prompt();
692
+ if (ctrlCTimer)
693
+ clearTimeout(ctrlCTimer);
694
+ ctrlCTimer = setTimeout(() => { ctrlCCount = 0; }, 2000);
695
+ });
696
+ /** Register brain event handlers (voice, scope switch) — reusable for auto-start and /brain */
697
+ async function registerBrainHandlers() {
698
+ const { onBrainVoiceInput, onBrainScopeSwitch, pushScopeChange, onBrainModelActivate, pushModelActivated } = await import('../brain/generator.js');
699
+ onBrainVoiceInput((text) => {
700
+ process.stdout.write('\r\x1b[K');
701
+ process.stdout.write(` ${chalk.dim('\u{1F3A4} Voice:')} ${chalk.cyan(text)}\n`);
702
+ rl.write(null, { ctrl: true, name: 'u' });
703
+ rl.write(text);
704
+ });
705
+ onBrainModelActivate(async (modelName) => {
706
+ try {
707
+ // Ensure Ollama is registered as a provider
708
+ store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
709
+ store.switchProvider('ollama', modelName);
710
+ config = store.getAll();
711
+ provider = createProvider('ollama', 'ollama', modelName, 'http://localhost:11434/v1');
712
+ pushModelActivated(modelName);
713
+ process.stdout.write(`\n ${chalk.green('\u26A1')} Model activated: ${chalk.cyan(modelName)} ${chalk.dim('(ollama)')}\n`);
714
+ isAtPrompt = true;
715
+ rl.prompt();
716
+ }
717
+ catch (err) {
718
+ process.stdout.write(`\n ${chalk.red('Model activation failed:')} ${err}\n`);
719
+ isAtPrompt = true;
720
+ rl.prompt();
721
+ }
722
+ });
723
+ onBrainScopeSwitch(async (newScope) => {
724
+ if (newScope === brainScope)
725
+ return;
726
+ try {
727
+ if (spiralEngine) {
728
+ try {
729
+ spiralEngine.close();
730
+ }
731
+ catch { /* best effort */ }
732
+ }
733
+ brainScope = newScope;
734
+ if (newScope === 'project') {
735
+ const { mkdirSync, existsSync } = await import('node:fs');
736
+ const { join } = await import('node:path');
737
+ const helixDir = join(process.cwd(), '.helixmind');
738
+ if (!existsSync(helixDir))
739
+ mkdirSync(helixDir, { recursive: true });
740
+ }
741
+ spiralEngine = await initSpiralEngine(newScope);
742
+ const { exportBrainData } = await import('../brain/exporter.js');
743
+ const { startLiveBrain } = await import('../brain/generator.js');
744
+ await startLiveBrain(spiralEngine, project.name || 'HelixMind', newScope);
745
+ pushScopeChange(newScope);
746
+ const scopeLabel = newScope === 'project'
747
+ ? chalk.cyan('\u{1F4C1} project-local (.helixmind/)')
748
+ : chalk.dim('\u{1F310} global (~/.spiral-context/)');
749
+ process.stdout.write(`\n \u{1F9E0} Brain switched to ${scopeLabel}\n`);
750
+ isAtPrompt = true;
751
+ rl.prompt();
752
+ }
753
+ catch (err) {
754
+ process.stdout.write(`\n ${chalk.red('Brain switch failed:')} ${err}\n`);
755
+ isAtPrompt = true;
756
+ rl.prompt();
757
+ }
758
+ });
759
+ }
760
+ // Register brain handlers if server was started during startup
761
+ if (brainUrl) {
762
+ registerBrainHandlers().catch(() => { });
763
+ }
764
+ // Keybinding state for double-ESC detection
765
+ const keyState = createKeybindingState();
766
+ // Handle raw keypresses for double-ESC + command suggestions
767
+ if (process.stdin.isTTY) {
768
+ process.stdin.on('keypress', async (_str, key) => {
769
+ if (!key)
770
+ return;
771
+ // === Tab switching: Ctrl+PageUp / Ctrl+PageDown ===
772
+ if (key.ctrl && key.name === 'pageup') {
773
+ sessionMgr.switchPrev();
774
+ writeTabBar();
775
+ return;
776
+ }
777
+ if (key.ctrl && key.name === 'pagedown') {
778
+ sessionMgr.switchNext();
779
+ writeTabBar();
780
+ return;
781
+ }
782
+ // === Command Suggestions ===
783
+ if (!agentRunning) {
784
+ const currentLine = (rl.line || '');
785
+ if (currentLine.startsWith('/') && currentLine.length >= 2) {
786
+ const suggestions = getSuggestions(currentLine);
787
+ // Clear old suggestions
788
+ if (lastSuggestionCount > 0)
789
+ clearSuggestions(lastSuggestionCount);
790
+ // Show new ones
791
+ if (suggestions.length > 0)
792
+ writeSuggestions(suggestions);
793
+ lastSuggestionCount = suggestions.length;
794
+ }
795
+ else {
796
+ // Clear suggestions when not typing a command
797
+ if (lastSuggestionCount > 0) {
798
+ clearSuggestions(lastSuggestionCount);
799
+ lastSuggestionCount = 0;
800
+ }
801
+ }
802
+ }
803
+ // === ESC detection ===
804
+ // Single ESC stops immediately when agent is running
805
+ // Double ESC works as fallback anytime
806
+ if (key.name === 'escape') {
807
+ if (agentRunning || sessionMgr.hasBackgroundTasks || autonomousMode) {
808
+ // Clear any suggestions
809
+ if (lastSuggestionCount > 0) {
810
+ clearSuggestions(lastSuggestionCount);
811
+ lastSuggestionCount = 0;
812
+ }
813
+ // IMMEDIATE STOP — single ESC press
814
+ activity.stop('Stopped');
815
+ agentController.abort();
816
+ sessionMgr.abortAll();
817
+ autonomousMode = false;
818
+ // Clear type-ahead buffer to prevent agent restarting after abort
819
+ typeAheadBuffer.length = 0;
820
+ // Reset agent state immediately (don't wait for async propagation)
821
+ agentRunning = false;
822
+ process.stdout.write('\n');
823
+ renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 All agents interrupted.'));
824
+ // Restore prompt so user can type again
825
+ showPrompt();
826
+ return;
827
+ }
828
+ }
829
+ // Double-ESC detection (for checkpoint browser when nothing is running)
830
+ const result = processKeypress(key, keyState);
831
+ if (result.action === 'open_browser' && !agentRunning) {
832
+ // Open checkpoint browser
833
+ rl.pause();
834
+ try {
835
+ const browserResult = await runCheckpointBrowser({
836
+ store: checkpointStore,
837
+ agentHistory,
838
+ simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
839
+ isPaused: false,
840
+ });
841
+ if (browserResult.action === 'revert') {
842
+ const r = browserResult.result;
843
+ process.stdout.write('\n');
844
+ if (r.messagesRemoved > 0)
845
+ renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
846
+ if (r.filesReverted > 0)
847
+ renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
848
+ }
849
+ }
850
+ catch {
851
+ // Browser closed unexpectedly
852
+ }
853
+ rl.resume();
854
+ showPrompt();
855
+ }
856
+ });
857
+ }
858
+ // Update statusbar — uses save/restore cursor (DECSC/DECRC).
859
+ // Only called during agent work to update token counts etc.
860
+ function updateStatusBar() {
861
+ if (!process.stdout.isTTY)
862
+ return;
863
+ const data = getStatusBarData();
864
+ writeStatusBar(data);
865
+ // Draw tab bar if there are background sessions, otherwise clear stale tab bar
866
+ if (sessionMgr.all.length > 1) {
867
+ writeTabBar();
868
+ }
869
+ else {
870
+ // Clear the tab bar row when no background sessions remain
871
+ clearTabBarRow();
872
+ }
873
+ }
874
+ /** Clear the tab bar row (row N-1) to remove stale tab bar text */
875
+ function clearTabBarRow() {
876
+ if (!process.stdout.isTTY)
877
+ return;
878
+ const termHeight = process.stdout.rows || 24;
879
+ process.stdout.write(`\x1b7` + // Save cursor
880
+ `\x1b[${termHeight - 1};0H` + // Move to tab bar row
881
+ `\x1b[2K` + // Clear line
882
+ `\x1b8`);
883
+ }
884
+ /** Draw the session tab bar above the statusbar */
885
+ function writeTabBar() {
886
+ if (!process.stdout.isTTY)
887
+ return;
888
+ if (sessionMgr.all.length <= 1)
889
+ return;
890
+ const tabBar = sessionMgr.renderTabs();
891
+ const termHeight = process.stdout.rows || 24;
892
+ const termWidth = (process.stdout.columns || 80) - 2;
893
+ // Truncate tab bar to terminal width to prevent overflow into other rows
894
+ const safeTabBar = truncateBar(tabBar, termWidth);
895
+ // Write tab bar above the statusbar (termHeight - 1)
896
+ // Layout: ..., tabbar(N-1), statusbar(N)
897
+ process.stdout.write(`\x1b7` + // Save cursor
898
+ `\x1b[${termHeight - 1};0H` + // Move to row above statusbar
899
+ `\x1b[2K` + // Clear line
900
+ ` ${safeTabBar}` + // Tab bar (truncated to fit)
901
+ `\x1b8`);
902
+ }
903
+ /** Push session findings to brain visualization */
904
+ function pushFindingsToBrain(session) {
905
+ import('../brain/generator.js').then(mod => {
906
+ if (!mod.isBrainServerRunning())
907
+ return;
908
+ const text = session.result?.text || '';
909
+ const sessionName = session.name;
910
+ // Parse findings from security/auto output — look for severity markers
911
+ const severityPatterns = [
912
+ { regex: /\*\*CRITICAL\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'critical' },
913
+ { regex: /\*\*HIGH\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'high' },
914
+ { regex: /\*\*MEDIUM\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'medium' },
915
+ { regex: /\*\*LOW\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'low' },
916
+ { regex: /DONE:\s*(.+?)(?:\n|$)/gi, severity: 'info' },
917
+ ];
918
+ for (const { regex, severity } of severityPatterns) {
919
+ let match;
920
+ while ((match = regex.exec(text)) !== null) {
921
+ const finding = match[1].trim();
922
+ if (finding.length > 5) {
923
+ // Try to extract file path from the finding text
924
+ const fileMatch = finding.match(/(?:in |file[:\s]+|path[:\s]+)([^\s,]+\.\w+)/i);
925
+ mod.pushAgentFinding(sessionName, finding, severity, fileMatch?.[1]);
926
+ }
927
+ }
928
+ }
929
+ // If no structured findings found but text exists, push a summary
930
+ if (text.length > 20) {
931
+ const lines = text.split('\n').filter(l => l.trim());
932
+ if (lines.length > 0) {
933
+ const summary = lines[0].slice(0, 120);
934
+ mod.pushAgentFinding(sessionName, summary, 'info');
935
+ }
936
+ }
937
+ }).catch(() => { });
938
+ }
939
+ // Set forward-reference for session completion callback
940
+ pushFindingsToBrainFn = pushFindingsToBrain;
941
+ // Type-ahead buffer: stores user input submitted while agent is running
942
+ const typeAheadBuffer = [];
943
+ // Paste detection — collects rapid-fire line events into a buffer
944
+ let pasteBuffer = [];
945
+ let pasteTimer = null;
946
+ const PASTE_THRESHOLD_MS = 50; // Lines arriving faster than this = paste
947
+ // Show full prompt area on startup (separator + status + > prompt)
948
+ showPrompt();
949
+ // Footer timer — redraws status bar during agent work (absolute positioning).
950
+ // Skipped when:
951
+ // - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
952
+ // - activity indicator is animating — prevents flicker collision
953
+ // - inline progress active (inlineProgressActive) — prevents flicker over feed progress
954
+ const footerTimer = setInterval(() => {
955
+ if (process.stdout.isTTY && !isAtPrompt && !activity.isAnimating && !inlineProgressActive)
956
+ updateStatusBar();
957
+ }, 500);
958
+ footerTimer.unref();
959
+ /** Process a complete input (single line or assembled paste block) */
960
+ async function processInput(input) {
961
+ // Handle /feed directly here (needs access to inlineProgressActive flag)
962
+ if (input.startsWith('/feed')) {
963
+ if (spiralEngine) {
964
+ const feedPath = input.split(/\s+/)[1];
965
+ const rootDir = process.cwd();
966
+ renderInfo('\u{1F300} Feeding project...\n');
967
+ try {
968
+ const result = await runFeedPipeline(rootDir, spiralEngine, {
969
+ targetPath: feedPath,
970
+ onProgress: wrappedFeedProgress,
971
+ });
972
+ renderFeedSummary(result);
973
+ checkpointStore.create({
974
+ type: 'feed',
975
+ label: `Feed ${feedPath || './'}`,
976
+ messageIndex: agentHistory.length,
977
+ });
978
+ }
979
+ catch (err) {
980
+ inlineProgressActive = false;
981
+ renderError(`Feed failed: ${err}`);
982
+ }
983
+ }
984
+ else {
985
+ renderInfo('Spiral engine not available.');
986
+ }
987
+ showPrompt();
988
+ return;
989
+ }
990
+ // Handle slash commands
991
+ if (input.startsWith('/')) {
992
+ const handled = await handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, { input: sessionTokensInput, output: sessionTokensOutput }, sessionToolCalls, (newProvider) => { provider = newProvider; config = store.getAll(); }, async (newScope) => {
993
+ // Switch brain scope
994
+ if (spiralEngine) {
995
+ try {
996
+ spiralEngine.close();
997
+ }
998
+ catch { /* best effort */ }
999
+ }
1000
+ brainScope = newScope;
1001
+ // Create .helixmind/ dir if switching to project and it doesn't exist
1002
+ if (newScope === 'project') {
1003
+ const { mkdirSync, existsSync } = await import('node:fs');
1004
+ const projDir = join(process.cwd(), '.helixmind');
1005
+ if (!existsSync(projDir)) {
1006
+ mkdirSync(projDir, { recursive: true });
1007
+ renderInfo(chalk.dim(' Created .helixmind/ directory'));
1008
+ }
1009
+ }
1010
+ spiralEngine = await initSpiralEngine(newScope);
1011
+ }, brainScope, async (action, goal) => {
1012
+ if (action === 'stop') {
1013
+ // Stop all background sessions + autonomous mode
1014
+ const running = sessionMgr.running;
1015
+ if (running.length > 0) {
1016
+ for (const s of running) {
1017
+ s.abort();
1018
+ renderInfo(`\u23F9 Stopped: ${s.icon} ${s.name}`);
1019
+ }
1020
+ autonomousMode = false;
1021
+ updateStatusBar();
1022
+ }
1023
+ else if (autonomousMode) {
1024
+ autonomousMode = false;
1025
+ agentController.abort();
1026
+ renderInfo('\u23F9 Stopping autonomous mode...');
1027
+ }
1028
+ else {
1029
+ renderInfo('No background sessions running.');
1030
+ }
1031
+ return;
1032
+ }
1033
+ if (action === 'security') {
1034
+ // Security audit — runs as BACKGROUND SESSION
1035
+ const bgSession = sessionMgr.create('\u{1F512} Security', '\u{1F512}', agentHistory);
1036
+ bgSession.start();
1037
+ renderInfo(`${chalk.hex('#00d4ff')('\u{1F512}')} Security audit started ${chalk.dim(`[session ${bgSession.id}]`)}`);
1038
+ updateStatusBar();
1039
+ // Run in background — don't await, user gets prompt back immediately
1040
+ runBackgroundSession(bgSession, SECURITY_PROMPT, provider, project, spiralEngine, config, permissions, checkpointStore, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }).then(result => {
1041
+ sessionMgr.complete(bgSession.id, result);
1042
+ }).catch(err => {
1043
+ if (!(err instanceof AgentAbortError)) {
1044
+ sessionMgr.complete(bgSession.id, {
1045
+ text: '',
1046
+ steps: [],
1047
+ errors: [err instanceof Error ? err.message : String(err)],
1048
+ durationMs: bgSession.elapsed,
1049
+ });
1050
+ }
1051
+ });
1052
+ return;
1053
+ }
1054
+ if (action === 'start') {
1055
+ // Check if autonomous is already running
1056
+ const existingAuto = sessionMgr.background.find(s => s.name.includes('Auto') && s.status === 'running');
1057
+ if (existingAuto) {
1058
+ renderInfo('Autonomous mode already running.');
1059
+ return;
1060
+ }
1061
+ // Enter autonomous mode as BACKGROUND SESSION
1062
+ autonomousMode = true;
1063
+ const sessionName = goal ? `\u{1F504} Auto: ${goal.slice(0, 30)}` : '\u{1F504} Auto';
1064
+ const bgSession = sessionMgr.create(sessionName, '\u{1F504}', agentHistory);
1065
+ bgSession.start();
1066
+ const goalHint = goal ? ` \u2014 ${chalk.white(goal.length > 50 ? goal.slice(0, 47) + '...' : goal)}` : '';
1067
+ renderInfo(`${chalk.hex('#ff6600')('\u{1F504}')} Autonomous mode started${goalHint} ${chalk.dim(`[session ${bgSession.id}]`)}`);
1068
+ updateStatusBar();
1069
+ // Run in background — user keeps their prompt
1070
+ (async () => {
1071
+ const completed = [];
1072
+ try {
1073
+ await runAutonomousLoop({
1074
+ sendMessage: async (prompt) => {
1075
+ bgSession.controller.reset();
1076
+ const resultTextHolder = { text: '' };
1077
+ const origAddSummary = bgSession.buffer.addAssistantSummary.bind(bgSession.buffer);
1078
+ bgSession.buffer.addAssistantSummary = (t) => {
1079
+ resultTextHolder.text = t;
1080
+ origAddSummary(t);
1081
+ };
1082
+ await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
1083
+ bgSession.buffer.addAssistantSummary = origAddSummary;
1084
+ return resultTextHolder.text;
1085
+ },
1086
+ isAborted: () => !autonomousMode || bgSession.controller.isAborted,
1087
+ onRoundStart: (round) => {
1088
+ bgSession.controller.reset();
1089
+ bgSession.capture(`Round ${round}...`);
1090
+ },
1091
+ onRoundEnd: (_round, summary) => {
1092
+ completed.push(summary);
1093
+ bgSession.capture(`\u2713 ${summary}`);
1094
+ updateStatusBar();
1095
+ },
1096
+ updateStatus: () => updateStatusBar(),
1097
+ }, goal);
1098
+ }
1099
+ catch (err) {
1100
+ if (!(err instanceof AgentAbortError)) {
1101
+ bgSession.capture(`Error: ${err}`);
1102
+ }
1103
+ }
1104
+ autonomousMode = false;
1105
+ sessionMgr.complete(bgSession.id, {
1106
+ text: completed.join('\n'),
1107
+ steps: [],
1108
+ errors: bgSession.controller.isAborted ? ['Aborted by user'] : [],
1109
+ durationMs: bgSession.elapsed,
1110
+ });
1111
+ updateStatusBar();
1112
+ })();
1113
+ }
1114
+ }, (action) => {
1115
+ // /validation handler
1116
+ switch (action) {
1117
+ case 'on':
1118
+ validationEnabled = true;
1119
+ renderInfo('Validation Matrix: ON');
1120
+ break;
1121
+ case 'off':
1122
+ validationEnabled = false;
1123
+ renderInfo('Validation Matrix: OFF');
1124
+ break;
1125
+ case 'verbose':
1126
+ validationVerbose = !validationVerbose;
1127
+ renderInfo(`Validation verbose: ${validationVerbose ? 'ON' : 'OFF'}`);
1128
+ break;
1129
+ case 'strict':
1130
+ validationStrict = !validationStrict;
1131
+ renderInfo(`Validation strict: ${validationStrict ? 'ON' : 'OFF'}`);
1132
+ break;
1133
+ case 'stats':
1134
+ getValidationStats(spiralEngine).then(stats => {
1135
+ if (stats) {
1136
+ process.stdout.write(renderValidationStats(stats));
1137
+ }
1138
+ else {
1139
+ renderInfo('No validation statistics yet.');
1140
+ }
1141
+ }).catch(() => renderInfo('Could not load stats.'));
1142
+ break;
1143
+ default:
1144
+ renderInfo(`Validation Matrix: ${validationEnabled ? 'ON' : 'OFF'} | Verbose: ${validationVerbose ? 'ON' : 'OFF'} | Strict: ${validationStrict ? 'ON' : 'OFF'}`);
1145
+ }
1146
+ }, sessionMgr, registerBrainHandlers, (active) => { isAtPrompt = active; });
1147
+ if (handled === 'exit') {
1148
+ spiralEngine?.close();
1149
+ rl.close();
1150
+ process.exit(0);
1151
+ }
1152
+ if (handled === 'drain') {
1153
+ // Sub-menu used its own readline — ignore line events for 500ms
1154
+ drainUntil = Date.now() + 500;
1155
+ }
1156
+ showPrompt();
1157
+ return;
1158
+ }
1159
+ // Render user message explicitly so it persists in the chat scroll history.
1160
+ // Readline's prompt echo can be overwritten by activity indicator / agent output.
1161
+ renderUserMessage(input);
1162
+ // Track user message in session buffer
1163
+ sessionBuffer.addUserMessage(input);
1164
+ // Create checkpoint for user message
1165
+ checkpointStore.createForChat(input, agentHistory.length);
1166
+ // === TYPE-AHEAD SUPPORT ===
1167
+ // Don't pause readline — let user type next prompt while agent works.
1168
+ // Buffer submitted lines during agent execution for processing after.
1169
+ // Show hint so user knows they can still type.
1170
+ process.stdout.write(chalk.dim(' \u{1F4AC} Type-ahead active \u2014 input queued for after agent finishes\n'));
1171
+ // Send message through agent loop
1172
+ roundToolCalls = 0;
1173
+ agentRunning = true;
1174
+ agentController.reset();
1175
+ updateStatusBar();
1176
+ await sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => {
1177
+ sessionTokensInput += inp;
1178
+ sessionTokensOutput += out;
1179
+ }, () => {
1180
+ sessionToolCalls++;
1181
+ roundToolCalls++;
1182
+ }, () => {
1183
+ // Activity started — readline stays active for type-ahead buffering
1184
+ // but we do NOT show a visible prompt (it would collide with tool output)
1185
+ isAtPrompt = false;
1186
+ }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
1187
+ agentRunning = false;
1188
+ // Keep simple message history for state persistence
1189
+ messages.push({ role: 'user', content: input });
1190
+ // Process any type-ahead input that was buffered during agent work
1191
+ // Skip if agent was aborted (ESC already cleared the buffer, but guard against race)
1192
+ while (typeAheadBuffer.length > 0 && !agentController.isAborted) {
1193
+ const buffered = typeAheadBuffer.shift();
1194
+ if (buffered.trim()) {
1195
+ // Process the buffered input as if user just typed it
1196
+ sessionBuffer.addUserMessage(buffered.trim());
1197
+ checkpointStore.createForChat(buffered.trim(), agentHistory.length);
1198
+ roundToolCalls = 0;
1199
+ agentRunning = true;
1200
+ agentController.reset();
1201
+ updateStatusBar();
1202
+ await sendAgentMessage(buffered.trim(), agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = true; rl.prompt(); }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
1203
+ agentRunning = false;
1204
+ messages.push({ role: 'user', content: buffered.trim() });
1205
+ }
1206
+ }
1207
+ showPrompt();
1208
+ }
1209
+ // === Paste-aware line handler ===
1210
+ // Rapid-fire line events (< 50ms apart) = multi-line paste.
1211
+ // We collect them and show a preview instead of sending immediately.
1212
+ rl.on('line', (line) => {
1213
+ isAtPrompt = false;
1214
+ ctrlCCount = 0;
1215
+ // Guard: skip phantom line events from sub-readline
1216
+ if (Date.now() < drainUntil) {
1217
+ showPrompt();
1218
+ return;
1219
+ }
1220
+ // Clear command suggestions on submit
1221
+ if (lastSuggestionCount > 0) {
1222
+ clearSuggestions(lastSuggestionCount);
1223
+ lastSuggestionCount = 0;
1224
+ }
1225
+ const trimmed = line.trim();
1226
+ // If paste buffer has content and user pressed Enter on empty line → send it
1227
+ if (!trimmed && pasteBuffer.length > 0) {
1228
+ const assembled = pasteBuffer.join('\n');
1229
+ pasteBuffer = [];
1230
+ if (pasteTimer) {
1231
+ clearTimeout(pasteTimer);
1232
+ pasteTimer = null;
1233
+ }
1234
+ // Show full pasted text as user message
1235
+ process.stdout.write(`\x1b[2K\r`);
1236
+ processInput(assembled);
1237
+ return;
1238
+ }
1239
+ if (!trimmed) {
1240
+ isAtPrompt = true;
1241
+ rl.prompt();
1242
+ return;
1243
+ }
1244
+ // If agent is running, buffer for type-ahead (no paste detection needed)
1245
+ if (agentRunning) {
1246
+ typeAheadBuffer.push(trimmed);
1247
+ process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${theme.dim(trimmed)}\n`);
1248
+ // Re-show prompt for further type-ahead input
1249
+ rl.prompt();
1250
+ return;
1251
+ }
1252
+ // Paste detection: if a timer is already running, this is a continuation
1253
+ if (pasteTimer) {
1254
+ pasteBuffer.push(line);
1255
+ clearTimeout(pasteTimer);
1256
+ // Show updated preview
1257
+ const count = pasteBuffer.length;
1258
+ process.stdout.write(`\x1b[2K\r ${chalk.dim(`(${count} Zeilen eingefuegt — Enter zum Senden, Esc zum Verwerfen)`)}`);
1259
+ pasteTimer = setTimeout(() => {
1260
+ // Paste ended — show final preview and wait for Enter
1261
+ pasteTimer = null;
1262
+ const count = pasteBuffer.length;
1263
+ const preview = pasteBuffer[0].slice(0, 60);
1264
+ process.stdout.write(`\x1b[2K\r ${chalk.cyan(`[${count} Zeilen]`)} ${chalk.dim(preview + (pasteBuffer[0].length > 60 ? '...' : ''))}\n`);
1265
+ process.stdout.write(` ${chalk.dim('Enter = senden | Esc = verwerfen')}\n`);
1266
+ rl.prompt();
1267
+ }, PASTE_THRESHOLD_MS);
1268
+ return;
1269
+ }
1270
+ // First line — start the paste timer
1271
+ pasteBuffer = [line];
1272
+ pasteTimer = setTimeout(() => {
1273
+ // Timer expired without more lines → this was a normal single-line input
1274
+ pasteTimer = null;
1275
+ const singleInput = pasteBuffer.join('\n').trim();
1276
+ pasteBuffer = [];
1277
+ if (singleInput) {
1278
+ processInput(singleInput);
1279
+ }
1280
+ else {
1281
+ isAtPrompt = true;
1282
+ rl.prompt();
1283
+ }
1284
+ }, PASTE_THRESHOLD_MS);
1285
+ });
1286
+ // Handle Esc to discard paste buffer
1287
+ if (process.stdin.isTTY) {
1288
+ const origKeypress = process.stdin.listeners('keypress');
1289
+ // Insert paste-cancel before the existing ESC handler
1290
+ process.stdin.prependListener('keypress', (_str, key) => {
1291
+ if (key?.name === 'escape' && pasteBuffer.length > 0 && !agentRunning) {
1292
+ pasteBuffer = [];
1293
+ if (pasteTimer) {
1294
+ clearTimeout(pasteTimer);
1295
+ pasteTimer = null;
1296
+ }
1297
+ process.stdout.write(`\x1b[2K\r ${chalk.dim('Paste verworfen.')}\n`);
1298
+ showPrompt();
1299
+ }
1300
+ });
1301
+ }
1302
+ rl.on('close', async () => {
1303
+ clearInterval(footerTimer);
1304
+ if (spiralEngine) {
1305
+ // Persist session buffer (goals, entities, decisions) into spiral brain
1306
+ // so next session with the same brain can recall them
1307
+ try {
1308
+ const goals = sessionBuffer.getGoals();
1309
+ const entities = sessionBuffer.getEntities();
1310
+ if (goals.length > 0) {
1311
+ await spiralEngine.store(`[Session Goals] ${goals.join(' | ')}`, 'decision', { tags: ['session', 'goals'] });
1312
+ }
1313
+ if (entities.size > 0) {
1314
+ const entryList = [...entities.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
1315
+ await spiralEngine.store(`[Session Refs] ${entryList}`, 'summary', { tags: ['session', 'entities'] });
1316
+ }
1317
+ }
1318
+ catch { /* best effort */ }
1319
+ try {
1320
+ await spiralEngine.saveState(messages);
1321
+ }
1322
+ catch { /* best effort */ }
1323
+ spiralEngine.close();
1324
+ }
1325
+ process.stdout.write('\n');
1326
+ process.exit(0);
1327
+ });
1328
+ }
1329
+ /**
1330
+ * Run an agent task in a background session.
1331
+ * Uses the session's own history, buffer, controller, and undo stack.
1332
+ * Output goes to the session's capture buffer (not stdout).
1333
+ */
1334
+ async function runBackgroundSession(session, prompt, provider, project, spiralEngine, config, permissions, checkpointStore, onTokens, onToolCall, validationOpts) {
1335
+ const bgActivity = new ActivityIndicator();
1336
+ await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, onTokens, onToolCall, undefined, validationOpts);
1337
+ // Build result from the session buffer
1338
+ const steps = session.buffer.getRecentErrors().map((e, i) => ({
1339
+ num: i + 1,
1340
+ tool: 'background',
1341
+ label: e.summary,
1342
+ status: 'error',
1343
+ error: e.summary,
1344
+ }));
1345
+ return {
1346
+ text: session.buffer.buildContext(),
1347
+ steps,
1348
+ errors: [],
1349
+ durationMs: session.elapsed,
1350
+ };
1351
+ }
1352
+ async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts) {
1353
+ // User message was rendered by renderUserMessage() in the caller before entering here.
1354
+ // Intent Detection: Check if user wants to feed the codebase
1355
+ const feedIntent = detectFeedIntent(input);
1356
+ if (feedIntent.detected && feedIntent.confidence > 0.7 && spiralEngine) {
1357
+ renderInfo('\u{1F300} Analyzing project in the background...\n');
1358
+ const rootDir = process.cwd();
1359
+ // Background feed runs silently (no progress output) to avoid colliding
1360
+ // with the activity indicator which also writes \r\x1b[K on the same line.
1361
+ runFeedPipeline(rootDir, spiralEngine, {
1362
+ targetPath: feedIntent.path,
1363
+ }).then(result => {
1364
+ if (result.nodesCreated > 0) {
1365
+ process.stdout.write(chalk.dim(` \u{1F300} Feed: +${result.nodesCreated} nodes from ${result.filesRead} files\n`));
1366
+ }
1367
+ }).catch(() => { });
1368
+ }
1369
+ // === WEB ENRICHMENT (background) ===
1370
+ // Automatically fetch web knowledge about the topic while the agent works.
1371
+ // Runs in background — results are stored in spiral brain for this + future queries.
1372
+ // Available for ALL tiers — this is the core intelligence that makes HelixMind useful.
1373
+ let enrichmentPromise = null;
1374
+ if (spiralEngine) {
1375
+ try {
1376
+ const { enrichFromWeb } = await import('../../spiral/cloud/web-enricher.js');
1377
+ const { pushWebKnowledge, isBrainServerRunning } = await import('../brain/generator.js');
1378
+ enrichmentPromise = enrichFromWeb(input, spiralEngine, {
1379
+ maxTopics: 2,
1380
+ maxPagesPerTopic: 2,
1381
+ minQuality: 0.4,
1382
+ onKnowledgeFound: (topic, summary, source) => {
1383
+ // Push live update to brain visualization (if open in browser)
1384
+ if (isBrainServerRunning()) {
1385
+ pushWebKnowledge(topic, summary, source);
1386
+ }
1387
+ },
1388
+ }).catch(() => null);
1389
+ }
1390
+ catch {
1391
+ // Web enrichment module not available, continue without
1392
+ }
1393
+ }
1394
+ // Query spiral context for system prompt enrichment
1395
+ let spiralContext = {
1396
+ level_1: [], level_2: [], level_3: [], level_4: [], level_5: [],
1397
+ total_tokens: 0, node_count: 0,
1398
+ };
1399
+ if (spiralEngine) {
1400
+ try {
1401
+ spiralContext = await spiralEngine.query(input, config.spiral.maxTokensBudget);
1402
+ }
1403
+ catch {
1404
+ // Spiral query failed, continue without
1405
+ }
1406
+ }
1407
+ // Assemble system prompt with spiral context + project info + session memory
1408
+ const sessionContext = sessionBuffer.buildContext();
1409
+ const systemPrompt = assembleSystemPrompt(project.name !== 'unknown' ? project : null, spiralContext, sessionContext || undefined, { provider: provider.name, model: provider.model });
1410
+ // Auto-trim context when approaching budget limit
1411
+ const maxBudget = config.spiral.maxTokensBudget || 200000;
1412
+ trimConversation(agentHistory, maxBudget, sessionBuffer);
1413
+ // Start the glowing activity indicator (reserves bottom row via scroll region)
1414
+ activity.start();
1415
+ // Notify caller so it can show the readline prompt for type-ahead
1416
+ onAgentStart?.();
1417
+ try {
1418
+ const result = await runAgentLoop(input, agentHistory, {
1419
+ provider,
1420
+ systemPrompt,
1421
+ permissions,
1422
+ toolContext: {
1423
+ projectRoot: process.cwd(),
1424
+ undoStack,
1425
+ spiralEngine,
1426
+ },
1427
+ checkpointStore,
1428
+ sessionBuffer,
1429
+ onThinking: () => {
1430
+ // Resume animation before each LLM call (timer keeps running)
1431
+ activity.setBlockMode(isInsideToolBlock());
1432
+ if (!activity.isAnimating) {
1433
+ activity.resumeAnimation();
1434
+ }
1435
+ },
1436
+ onTokensUsed: (inp, out) => {
1437
+ onTokens(inp, out);
1438
+ },
1439
+ onToolCall: () => {
1440
+ activity.pauseAnimation(); // Pause animation during tool execution (timer keeps running)
1441
+ onToolCall();
1442
+ },
1443
+ onStepStart: (num, _tool, label) => {
1444
+ activity.setStep(num, label);
1445
+ },
1446
+ onStepEnd: (_num, _tool, status) => {
1447
+ if (status === 'error')
1448
+ activity.setError();
1449
+ },
1450
+ onBeforeAnswer: () => {
1451
+ activity.stop(); // Writes colorful "HelixMind Done" replacing animation
1452
+ },
1453
+ }, controller);
1454
+ // activity.stop() was already called via onBeforeAnswer (shows colorful "Done" line)
1455
+ // Ensure stopped if onBeforeAnswer wasn't reached (e.g. no tools, direct answer)
1456
+ if (activity.isRunning)
1457
+ activity.stop();
1458
+ // CRITICAL: Adopt updated conversation history from agent loop.
1459
+ // runAgentLoop works on a copy — we must sync it back so the next turn
1460
+ // sees the full conversation (user message + assistant + tool results).
1461
+ agentHistory.length = 0;
1462
+ agentHistory.push(...result.updatedHistory);
1463
+ // ═══ PHASE 3: VALIDATION MATRIX ═══
1464
+ if (validationOpts?.enabled && result.text) {
1465
+ try {
1466
+ // Phase 1: Classify
1467
+ const classification = classifyTask(input);
1468
+ if (classification.category !== 'chat_only') {
1469
+ // Generate criteria
1470
+ let spiralContextStr = '';
1471
+ if (spiralEngine) {
1472
+ try {
1473
+ const sq = await spiralEngine.query(input, undefined, [3, 4, 5]);
1474
+ spiralContextStr = [...sq.level_3, ...sq.level_4, ...sq.level_5]
1475
+ .map((n) => n.content).join('\n');
1476
+ }
1477
+ catch { /* ignore */ }
1478
+ }
1479
+ const criteria = generateCriteria(classification, input, spiralContextStr || undefined);
1480
+ if (criteria.length > 0) {
1481
+ if (validationOpts.verbose) {
1482
+ process.stdout.write(renderClassification(classification.category, classification.complexity, criteria.length) + '\n');
1483
+ }
1484
+ process.stdout.write(renderValidationStart());
1485
+ // Create validation provider (smaller/faster model)
1486
+ let valProvider;
1487
+ try {
1488
+ valProvider = createValidationProvider(config.model, config.provider, config.apiKey);
1489
+ }
1490
+ catch { /* use without dynamic checks */ }
1491
+ // Run validation loop
1492
+ const valResult = await validationLoop(result.text, {
1493
+ criteria,
1494
+ userRequest: input,
1495
+ spiralContext: spiralContextStr,
1496
+ spiralEngine: spiralEngine || undefined,
1497
+ validationProvider: valProvider,
1498
+ maxLoops: 3,
1499
+ });
1500
+ // If strict mode, promote warnings to effective errors
1501
+ if (validationOpts.strict) {
1502
+ for (const r of valResult.results) {
1503
+ if (!r.passed && r.severity === 'warning') {
1504
+ r.severity = 'error';
1505
+ }
1506
+ }
1507
+ }
1508
+ // Show summary
1509
+ process.stdout.write(renderValidationSummary(valResult, validationOpts.verbose) + '\n');
1510
+ // Store stats in spiral
1511
+ await storeValidationResult(valResult, classification.category, spiralEngine || undefined);
1512
+ }
1513
+ }
1514
+ }
1515
+ catch {
1516
+ // Validation should never block the user
1517
+ }
1518
+ }
1519
+ // Track assistant response in session buffer
1520
+ if (result.text) {
1521
+ sessionBuffer.addAssistantSummary(result.text);
1522
+ }
1523
+ // Create checkpoint for agent response
1524
+ if (result.text) {
1525
+ checkpointStore.createForChat(result.text.length > 60 ? result.text.slice(0, 60) + '...' : result.text, agentHistory.length);
1526
+ }
1527
+ // Store turn summary in spiral (user request + agent response)
1528
+ if (spiralEngine && config.spiral.autoStore && result.text) {
1529
+ const turnSummary = `User: ${input.slice(0, 100)} → Agent: ${result.text.slice(0, 400)}`;
1530
+ spiralEngine.store(turnSummary, 'summary', { tags: ['session', 'turn'] }).catch(() => { });
1531
+ }
1532
+ // Show web enrichment results (if any arrived while agent worked)
1533
+ if (enrichmentPromise) {
1534
+ try {
1535
+ const enrichResult = await enrichmentPromise;
1536
+ if (enrichResult && enrichResult.nodesStored > 0) {
1537
+ const topicList = enrichResult.topics.join(', ');
1538
+ process.stdout.write(chalk.dim(` \u{1F310} Web: +${enrichResult.nodesStored} knowledge nodes stored `) +
1539
+ chalk.dim(`(${topicList})`) +
1540
+ chalk.dim(` [${enrichResult.duration_ms}ms]\n`));
1541
+ }
1542
+ }
1543
+ catch {
1544
+ // Enrichment error — silent, never block the user
1545
+ }
1546
+ }
1547
+ }
1548
+ catch (err) {
1549
+ if (activity.isRunning)
1550
+ activity.stop('Stopped');
1551
+ if (err instanceof AgentAbortError) {
1552
+ renderInfo('\n\u23F9 Agent aborted.');
1553
+ }
1554
+ else {
1555
+ const errMsg = err instanceof Error ? err.message : String(err);
1556
+ // Categorize and show user-friendly error
1557
+ const { isRateLimitError: isRL } = await import('../providers/rate-limiter.js');
1558
+ if (isRL(err)) {
1559
+ process.stdout.write('\n');
1560
+ renderError('Rate limit reached. Waiting and retrying automatically next time.');
1561
+ renderInfo(chalk.dim(' Tip: Use /compact to reduce spiral nodes, or wait a moment before retrying.'));
1562
+ }
1563
+ else if (errMsg.includes('authentication') || errMsg.includes('401') || errMsg.includes('invalid.*key')) {
1564
+ renderError('Authentication failed. Your API key may be invalid or expired.');
1565
+ renderInfo(chalk.dim(' Fix: /keys to update your API key.'));
1566
+ }
1567
+ else if (errMsg.includes('ENOTFOUND') || errMsg.includes('ECONNREFUSED') || errMsg.includes('network')) {
1568
+ renderError('Network error — cannot reach the API server.');
1569
+ renderInfo(chalk.dim(' Check your internet connection and try again.'));
1570
+ }
1571
+ else if (errMsg.includes('context_length') || errMsg.includes('too many tokens') || errMsg.includes('maximum context')) {
1572
+ renderError('Context too large for the model.');
1573
+ renderInfo(chalk.dim(' Fix: /clear to reset conversation, or /compact to reduce spiral size.'));
1574
+ }
1575
+ else if (errMsg.includes('Max retries exceeded')) {
1576
+ renderError('API temporarily unavailable after multiple retries.');
1577
+ renderInfo(chalk.dim(' Wait a moment and try again. The rate limiter will auto-recover.'));
1578
+ }
1579
+ else {
1580
+ renderError(errMsg.length > 200 ? errMsg.slice(0, 200) + '...' : errMsg);
1581
+ }
1582
+ // Track error in session buffer
1583
+ sessionBuffer.addToolError('agent_loop', errMsg);
1584
+ }
1585
+ }
1586
+ }
1587
+ function showSkipPermissionsWarning() {
1588
+ const w = chalk.yellow;
1589
+ const g = chalk.green;
1590
+ const d = chalk.dim;
1591
+ process.stdout.write('\n');
1592
+ process.stdout.write(d('\u256D\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\u2500\u2500\u2500\u2500\u256E') + '\n');
1593
+ process.stdout.write(d('\u2502 ') + w('\u26A0\uFE0F SKIP-PERMISSIONS MODE') + d(' \u2502') + '\n');
1594
+ process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
1595
+ process.stdout.write(d('\u2502 ') + 'HelixMind will automatically:' + d(' \u2502') + '\n');
1596
+ process.stdout.write(d('\u2502 ') + g('\u2713') + ' Read and write files' + d(' \u2502') + '\n');
1597
+ process.stdout.write(d('\u2502 ') + g('\u2713') + ' Edit existing code' + d(' \u2502') + '\n');
1598
+ process.stdout.write(d('\u2502 ') + g('\u2713') + ' Run shell commands (safe ones)' + d(' \u2502') + '\n');
1599
+ process.stdout.write(d('\u2502 ') + g('\u2713') + ' Create git commits' + d(' \u2502') + '\n');
1600
+ process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
1601
+ process.stdout.write(d('\u2502 ') + 'Still requires confirmation for:' + d(' \u2502') + '\n');
1602
+ process.stdout.write(d('\u2502 ') + w('\u26A0') + ' Dangerous commands (rm -rf, sudo)' + d(' \u2502') + '\n');
1603
+ process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
1604
+ process.stdout.write(d('\u2502 ') + d('ESC = stop agent --yolo = skip all') + d(' \u2502') + '\n');
1605
+ process.stdout.write(d('\u2570\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\u2500\u2500\u2500\u2500\u256F') + '\n\n');
1606
+ }
1607
+ function showFullAutonomousWarning() {
1608
+ const r = chalk.red;
1609
+ const d = chalk.dim;
1610
+ process.stdout.write('\n');
1611
+ process.stdout.write(d('\u256D\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\u2500\u2500\u2500\u2500\u256E') + '\n');
1612
+ process.stdout.write(d('\u2502 ') + r('\u{1F525} FULL AUTONOMOUS MODE') + d(' \u2502') + '\n');
1613
+ process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
1614
+ process.stdout.write(d('\u2502 ') + 'HelixMind will execute ALL actions' + d(' \u2502') + '\n');
1615
+ process.stdout.write(d('\u2502 ') + 'without asking. No confirmations.' + d(' \u2502') + '\n');
1616
+ process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
1617
+ process.stdout.write(d('\u2502 ') + d('ESC = stop agent if needed.') + d(' \u2502') + '\n');
1618
+ process.stdout.write(d('\u2570\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\u2500\u2500\u2500\u2500\u256F') + '\n\n');
1619
+ }
1620
+ async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt) {
1621
+ const parts = input.split(/\s+/);
1622
+ const cmd = parts[0].toLowerCase();
1623
+ switch (cmd) {
1624
+ case '/help': {
1625
+ if (!process.stdin.isTTY) {
1626
+ // Non-interactive: show static text
1627
+ process.stdout.write(HELP_TEXT);
1628
+ break;
1629
+ }
1630
+ // Pause readline so selectMenu can take raw input
1631
+ onSubPrompt?.(true);
1632
+ rl.pause();
1633
+ process.stdout.write('\n');
1634
+ const { items: helpItems, commands: helpCmds } = buildHelpMenuItems();
1635
+ const helpIdx = await selectMenu(helpItems, {
1636
+ title: chalk.hex('#00d4ff').bold('HelixMind Commands'),
1637
+ cancelLabel: 'Close',
1638
+ pageSize: 15,
1639
+ });
1640
+ rl.resume();
1641
+ if (helpIdx >= 0 && helpCmds[helpIdx]) {
1642
+ // Execute the selected command
1643
+ return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt);
1644
+ }
1645
+ break;
1646
+ }
1647
+ case '/clear':
1648
+ messages.length = 0;
1649
+ agentHistory.length = 0;
1650
+ renderInfo('Conversation cleared.');
1651
+ break;
1652
+ case '/model': {
1653
+ const directModel = parts[1];
1654
+ if (directModel) {
1655
+ // Check if it looks like an Ollama model (contains ':' or is a known local model pattern)
1656
+ const isOllamaModel = directModel.includes(':') || directModel.match(/^(qwen|llama|deepseek|codellama|mistral|phi|gemma|starcoder)/i);
1657
+ if (isOllamaModel) {
1658
+ // Switch to Ollama provider
1659
+ store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
1660
+ store.switchProvider('ollama', directModel);
1661
+ try {
1662
+ const newProvider = createProvider('ollama', 'ollama', directModel, 'http://localhost:11434/v1');
1663
+ onProviderSwitch?.(newProvider);
1664
+ renderInfo(`Switched to: ollama / ${directModel}`);
1665
+ }
1666
+ catch (err) {
1667
+ renderError(`Failed to switch: ${err}`);
1668
+ }
1669
+ }
1670
+ else {
1671
+ // Regular provider model switch
1672
+ store.switchModel(directModel);
1673
+ const newConfig = store.getAll();
1674
+ try {
1675
+ const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
1676
+ onProviderSwitch?.(newProvider);
1677
+ renderInfo(`Switched to: ${newConfig.provider} / ${newConfig.model}`);
1678
+ }
1679
+ catch (err) {
1680
+ renderError(`Failed to switch: ${err}`);
1681
+ }
1682
+ }
1683
+ }
1684
+ else {
1685
+ // Interactive picker — suppress statusbar to prevent cursor interference
1686
+ onSubPrompt?.(true);
1687
+ rl.pause();
1688
+ const configBefore = store.getAll();
1689
+ const result = await showModelSwitcher(store, rl);
1690
+ rl.resume();
1691
+ // Always refresh provider if config changed (covers "Add new provider" path too)
1692
+ const newConfig = store.getAll();
1693
+ const configChanged = newConfig.provider !== configBefore.provider
1694
+ || newConfig.model !== configBefore.model
1695
+ || newConfig.apiKey !== configBefore.apiKey;
1696
+ if ((result || configChanged) && onProviderSwitch && newConfig.apiKey) {
1697
+ try {
1698
+ const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
1699
+ onProviderSwitch(newProvider);
1700
+ }
1701
+ catch (err) {
1702
+ renderError(`Failed to switch: ${err}`);
1703
+ }
1704
+ }
1705
+ }
1706
+ return 'drain'; // Sub-readline may leave phantom line events
1707
+ }
1708
+ case '/keys': {
1709
+ // Suppress statusbar to prevent cursor interference during text input
1710
+ onSubPrompt?.(true);
1711
+ rl.pause();
1712
+ await showKeyManagement(store, rl);
1713
+ rl.resume();
1714
+ // Refresh provider after key changes
1715
+ const newConfig = store.getAll();
1716
+ if (newConfig.apiKey && onProviderSwitch) {
1717
+ try {
1718
+ const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
1719
+ onProviderSwitch(newProvider);
1720
+ }
1721
+ catch { /* ignore */ }
1722
+ }
1723
+ return 'drain'; // Sub-readline may leave phantom line events
1724
+ }
1725
+ case '/spiral':
1726
+ if (spiralEngine) {
1727
+ try {
1728
+ const status = spiralEngine.status();
1729
+ renderSpiralStatus(status.total_nodes, status.per_level[1] ?? 0, status.per_level[2] ?? 0, status.per_level[3] ?? 0, status.per_level[4] ?? 0, status.per_level[5] ?? 0);
1730
+ }
1731
+ catch {
1732
+ renderInfo('Spiral engine not available.');
1733
+ }
1734
+ }
1735
+ else {
1736
+ renderInfo('Spiral engine disabled.');
1737
+ }
1738
+ break;
1739
+ case '/helix':
1740
+ case '/helixlocal':
1741
+ // Always use local brain for /helix and /helixlocal
1742
+ if (onBrainSwitch && currentBrainScope !== 'project') {
1743
+ await onBrainSwitch('project');
1744
+ renderInfo(chalk.cyan('\u{1F4C1} Switched to project-local brain (.helixmind/)'));
1745
+ try {
1746
+ const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
1747
+ if (isBrainServerRunning())
1748
+ pushScopeChange('project');
1749
+ }
1750
+ catch { /* optional */ }
1751
+ }
1752
+ // Auto-start brain visualization
1753
+ if (spiralEngine) {
1754
+ try {
1755
+ const { exportBrainData } = await import('../brain/exporter.js');
1756
+ const { startLiveBrain, isBrainServerRunning } = await import('../brain/generator.js');
1757
+ const { exec } = await import('node:child_process');
1758
+ const { platform } = await import('node:os');
1759
+ const data = exportBrainData(spiralEngine, 'HelixMind Project', 'project');
1760
+ if (data.meta.totalNodes > 0 && !isBrainServerRunning()) {
1761
+ const url = await startLiveBrain(spiralEngine, 'HelixMind Project', 'project');
1762
+ if (onRegisterBrainHandlers)
1763
+ await onRegisterBrainHandlers();
1764
+ const openCmd = platform() === 'win32' ? `start "" "${url}"`
1765
+ : platform() === 'darwin' ? `open "${url}"`
1766
+ : `xdg-open "${url}"`;
1767
+ exec(openCmd, () => { });
1768
+ process.stdout.write(` ${theme.success('\u{1F9E0} Brain View started:')} ${url}\n`);
1769
+ }
1770
+ }
1771
+ catch { /* brain optional */ }
1772
+ }
1773
+ onSubPrompt?.(true);
1774
+ rl.pause();
1775
+ await showHelixMenu(spiralEngine, store, 'project');
1776
+ rl.resume();
1777
+ return 'drain';
1778
+ case '/helixglobal':
1779
+ // Use global brain for /helixglobal
1780
+ if (onBrainSwitch && currentBrainScope !== 'global') {
1781
+ await onBrainSwitch('global');
1782
+ renderInfo(chalk.dim('\u{1F310} Switched to global brain (~/.spiral-context/)'));
1783
+ try {
1784
+ const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
1785
+ if (isBrainServerRunning())
1786
+ pushScopeChange('global');
1787
+ }
1788
+ catch { /* optional */ }
1789
+ }
1790
+ // Auto-start brain visualization
1791
+ if (spiralEngine) {
1792
+ try {
1793
+ const { exportBrainData } = await import('../brain/exporter.js');
1794
+ const { startLiveBrain, isBrainServerRunning } = await import('../brain/generator.js');
1795
+ const { exec } = await import('node:child_process');
1796
+ const { platform } = await import('node:os');
1797
+ const data = exportBrainData(spiralEngine, 'HelixMind Project', 'global');
1798
+ if (data.meta.totalNodes > 0 && !isBrainServerRunning()) {
1799
+ const url = await startLiveBrain(spiralEngine, 'HelixMind Project', 'global');
1800
+ if (onRegisterBrainHandlers)
1801
+ await onRegisterBrainHandlers();
1802
+ const openCmd = platform() === 'win32' ? `start "" "${url}"`
1803
+ : platform() === 'darwin' ? `open "${url}"`
1804
+ : `xdg-open "${url}"`;
1805
+ exec(openCmd, () => { });
1806
+ process.stdout.write(` ${theme.success('\u{1F9E0} Brain View started:')} ${url}\n`);
1807
+ }
1808
+ }
1809
+ catch { /* brain optional */ }
1810
+ }
1811
+ onSubPrompt?.(true);
1812
+ rl.pause();
1813
+ await showHelixMenu(spiralEngine, store, 'global');
1814
+ rl.resume();
1815
+ return 'drain';
1816
+ case '/brain': {
1817
+ const brainArg = parts[1]?.toLowerCase();
1818
+ // /brain local — switch to project-local brain
1819
+ if (brainArg === 'local' || brainArg === 'project') {
1820
+ if (currentBrainScope === 'project') {
1821
+ renderInfo('Already using project-local brain.');
1822
+ }
1823
+ else if (onBrainSwitch) {
1824
+ await onBrainSwitch('project');
1825
+ renderInfo(chalk.cyan('\u{1F4C1} Switched to project-local brain (.helixmind/)'));
1826
+ // Update browser if open
1827
+ try {
1828
+ const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
1829
+ if (isBrainServerRunning())
1830
+ pushScopeChange('project');
1831
+ }
1832
+ catch { /* optional */ }
1833
+ }
1834
+ break;
1835
+ }
1836
+ // /brain global — switch to global brain
1837
+ if (brainArg === 'global') {
1838
+ if (currentBrainScope === 'global') {
1839
+ renderInfo('Already using global brain.');
1840
+ }
1841
+ else if (onBrainSwitch) {
1842
+ await onBrainSwitch('global');
1843
+ renderInfo(chalk.dim('\u{1F310} Switched to global brain (~/.spiral-context/)'));
1844
+ // Update browser if open
1845
+ try {
1846
+ const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
1847
+ if (isBrainServerRunning())
1848
+ pushScopeChange('global');
1849
+ }
1850
+ catch { /* optional */ }
1851
+ }
1852
+ break;
1853
+ }
1854
+ // /brain (no arg) — show status + open 3D view
1855
+ if (!brainArg) {
1856
+ const scopeLabel = currentBrainScope === 'project'
1857
+ ? chalk.cyan('project-local') + chalk.dim(' (.helixmind/)')
1858
+ : chalk.dim('global') + chalk.dim(' (~/.spiral-context/)');
1859
+ renderInfo(`Brain scope: ${scopeLabel}`);
1860
+ renderInfo(chalk.dim(' /brain local — switch to project brain'));
1861
+ renderInfo(chalk.dim(' /brain global — switch to global brain'));
1862
+ process.stdout.write('\n');
1863
+ }
1864
+ // Open 3D visualization (for /brain or /brain view)
1865
+ if (!brainArg || brainArg === 'view') {
1866
+ if (spiralEngine) {
1867
+ try {
1868
+ const { exportBrainData } = await import('../brain/exporter.js');
1869
+ const { startLiveBrain } = await import('../brain/generator.js');
1870
+ const { exec } = await import('node:child_process');
1871
+ const { platform } = await import('node:os');
1872
+ const data = exportBrainData(spiralEngine, 'HelixMind Project', currentBrainScope);
1873
+ if (data.meta.totalNodes === 0) {
1874
+ renderInfo('Spiral is empty. Feed some files first: /feed');
1875
+ break;
1876
+ }
1877
+ const url = await startLiveBrain(spiralEngine, 'HelixMind Project', currentBrainScope);
1878
+ // Register voice + scope switch handlers
1879
+ if (onRegisterBrainHandlers)
1880
+ await onRegisterBrainHandlers();
1881
+ const openCmd = platform() === 'win32' ? `start "" "${url}"`
1882
+ : platform() === 'darwin' ? `open "${url}"`
1883
+ : `xdg-open "${url}"`;
1884
+ exec(openCmd, () => { });
1885
+ process.stdout.write(` ${theme.success('\u{1F9E0} Brain View live at:')} ${url}\n`);
1886
+ renderInfo('Auto-updates when spiral changes. Voice input enabled via browser mic.');
1887
+ }
1888
+ catch (err) {
1889
+ renderError(`Brain view failed: ${err}`);
1890
+ }
1891
+ }
1892
+ else {
1893
+ renderInfo('Spiral engine not available.');
1894
+ }
1895
+ }
1896
+ break;
1897
+ }
1898
+ case '/feed':
1899
+ // Handled directly in chatCommand() for access to inlineProgressActive flag
1900
+ break;
1901
+ case '/context':
1902
+ if (spiralEngine) {
1903
+ const status = spiralEngine.status();
1904
+ renderInfo(`Context: ${status.total_nodes} spiral nodes, ${status.total_edges} edges`);
1905
+ renderInfo(` Storage: ${(status.storage_size_bytes / 1024).toFixed(1)} KB`);
1906
+ renderInfo(` Embeddings: ${status.embedding_status}`);
1907
+ renderInfo(` Session buffer: ${sessionBuffer.eventCount} events, ${sessionBuffer.totalErrors} errors`);
1908
+ renderInfo(` Files modified: ${sessionBuffer.getModifiedFiles().length}`);
1909
+ }
1910
+ else {
1911
+ renderInfo('Spiral engine not available.');
1912
+ }
1913
+ break;
1914
+ case '/project': {
1915
+ const { analyzeProject } = await import('../context/project.js');
1916
+ const proj = await analyzeProject(process.cwd());
1917
+ renderInfo(`Project: ${proj.name} (${proj.type})`);
1918
+ if (proj.frameworks?.length)
1919
+ renderInfo(` Frameworks: ${proj.frameworks.join(', ')}`);
1920
+ renderInfo(` Files: ${proj.files?.length ?? 'unknown'}`);
1921
+ break;
1922
+ }
1923
+ case '/compact':
1924
+ if (spiralEngine) {
1925
+ const result = spiralEngine.evolve();
1926
+ renderInfo(`Evolution: ${result.promoted} promoted, ${result.demoted} demoted, ${result.summarized} summarized`);
1927
+ }
1928
+ else {
1929
+ renderInfo('Spiral engine not available.');
1930
+ }
1931
+ break;
1932
+ case '/tokens':
1933
+ renderInfo(`Session tokens: ${sessionTokens.input} in, ${sessionTokens.output} out (${sessionTokens.input + sessionTokens.output} total)`);
1934
+ renderInfo(`Tool calls: ${sessionToolCalls}`);
1935
+ renderInfo(`Checkpoints: ${checkpointStore.count}`);
1936
+ renderInfo(`Memory (snapshots): ${(checkpointStore.memoryUsage / 1024).toFixed(1)} KB`);
1937
+ renderInfo(`Session buffer: ${sessionBuffer.eventCount} events`);
1938
+ break;
1939
+ case '/yolo': {
1940
+ const arg = parts[1]?.toLowerCase();
1941
+ if (arg === 'on') {
1942
+ permissions.setYolo(true);
1943
+ renderInfo('YOLO mode ON \u2014 ALL operations auto-approved');
1944
+ }
1945
+ else if (arg === 'off') {
1946
+ permissions.setYolo(false);
1947
+ renderInfo('YOLO mode OFF');
1948
+ }
1949
+ else {
1950
+ renderInfo(`YOLO mode: ${permissions.isYolo() ? 'ON' : 'OFF'}`);
1951
+ }
1952
+ break;
1953
+ }
1954
+ case '/skip-permissions': {
1955
+ const arg = parts[1]?.toLowerCase();
1956
+ if (arg === 'on') {
1957
+ permissions.setSkipPermissions(true);
1958
+ renderInfo('Skip-permissions ON \u2014 write operations auto-approved (dangerous still asks)');
1959
+ }
1960
+ else if (arg === 'off') {
1961
+ permissions.setSkipPermissions(false);
1962
+ renderInfo('Skip-permissions OFF \u2014 write operations require confirmation');
1963
+ }
1964
+ else {
1965
+ renderInfo(`Skip-permissions: ${permissions.isSkipPermissions() ? 'ON' : 'OFF'}`);
1966
+ }
1967
+ break;
1968
+ }
1969
+ case '/undo': {
1970
+ const countArg = parts[1];
1971
+ if (countArg === 'list') {
1972
+ const entries = undoStack.list();
1973
+ if (entries.length === 0) {
1974
+ renderInfo('No undo history.');
1975
+ }
1976
+ else {
1977
+ renderInfo(`Undo history (${entries.length} entries):`);
1978
+ for (const entry of entries.slice(0, 10)) {
1979
+ const age = Math.round((Date.now() - entry.timestamp) / 1000);
1980
+ renderInfo(` ${entry.tool}: ${entry.path} (${age}s ago)`);
1981
+ }
1982
+ }
1983
+ }
1984
+ else {
1985
+ const count = parseInt(countArg) || 1;
1986
+ const result = undoStack.undo(count);
1987
+ if (result.undone === 0) {
1988
+ renderInfo('Nothing to undo.');
1989
+ }
1990
+ else {
1991
+ renderInfo(`Undone ${result.undone} change(s):`);
1992
+ for (const entry of result.entries) {
1993
+ renderInfo(` Reverted: ${entry.path}`);
1994
+ }
1995
+ }
1996
+ }
1997
+ break;
1998
+ }
1999
+ case '/validation': {
2000
+ const vArg = parts[1]?.toLowerCase();
2001
+ if (onValidation) {
2002
+ onValidation(vArg || 'status');
2003
+ }
2004
+ break;
2005
+ }
2006
+ case '/diff':
2007
+ try {
2008
+ const { execSync } = await import('node:child_process');
2009
+ const diff = execSync('git diff', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
2010
+ if (diff) {
2011
+ process.stdout.write(`\n${diff}\n\n`);
2012
+ }
2013
+ else {
2014
+ renderInfo('No uncommitted changes.');
2015
+ }
2016
+ }
2017
+ catch {
2018
+ renderInfo('Not a git repository.');
2019
+ }
2020
+ break;
2021
+ case '/git':
2022
+ try {
2023
+ const { execSync } = await import('node:child_process');
2024
+ const status = execSync('git status --short', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
2025
+ const branch = execSync('git branch --show-current', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
2026
+ renderInfo(`Branch: ${branch}`);
2027
+ if (status) {
2028
+ process.stdout.write(`\n${status}\n\n`);
2029
+ }
2030
+ else {
2031
+ renderInfo('Working tree clean.');
2032
+ }
2033
+ }
2034
+ catch {
2035
+ renderInfo('Not a git repository.');
2036
+ }
2037
+ break;
2038
+ case '/export': {
2039
+ const outputDir = parts[1] || process.cwd();
2040
+ if (spiralEngine) {
2041
+ try {
2042
+ const { exportToZip } = await import('../brain/archive.js');
2043
+ const zipPath = exportToZip(spiralEngine, outputDir);
2044
+ renderInfo(`Exported to: ${zipPath}`);
2045
+ }
2046
+ catch (err) {
2047
+ renderError(`Export failed: ${err}`);
2048
+ }
2049
+ }
2050
+ else {
2051
+ renderInfo('Spiral engine not available.');
2052
+ }
2053
+ break;
2054
+ }
2055
+ case '/login': {
2056
+ const { loginCommand } = await import('./auth.js');
2057
+ await loginCommand({});
2058
+ return 'drain';
2059
+ }
2060
+ case '/logout': {
2061
+ const { logoutCommand } = await import('./auth.js');
2062
+ await logoutCommand({});
2063
+ return 'drain';
2064
+ }
2065
+ case '/whoami': {
2066
+ const { whoamiCommand } = await import('./auth.js');
2067
+ await whoamiCommand();
2068
+ return;
2069
+ }
2070
+ case '/exit':
2071
+ case '/quit': {
2072
+ // Stop live brain server
2073
+ try {
2074
+ const { stopLiveBrain } = await import('../brain/generator.js');
2075
+ stopLiveBrain();
2076
+ }
2077
+ catch { /* ignore */ }
2078
+ if (spiralEngine) {
2079
+ renderInfo('Saving state...');
2080
+ try {
2081
+ await spiralEngine.saveState(messages);
2082
+ }
2083
+ catch { /* best effort */ }
2084
+ }
2085
+ renderInfo('Goodbye!');
2086
+ return 'exit';
2087
+ }
2088
+ case '/auto':
2089
+ case '/dontstop': {
2090
+ if (!onAutonomous)
2091
+ break;
2092
+ // Extract goal text after "/auto " (e.g. "/auto fix all TypeScript errors")
2093
+ const autoGoal = input.replace(/^\/(auto|dontstop)\s*/i, '').trim() || undefined;
2094
+ if (autoGoal) {
2095
+ // Goal provided — start directly without confirmation menu
2096
+ await onAutonomous('start', autoGoal);
2097
+ }
2098
+ else {
2099
+ // No goal — show confirmation menu
2100
+ rl.pause();
2101
+ process.stdout.write('\n');
2102
+ const autoConfirm = await selectMenu([
2103
+ { label: chalk.hex('#ff6600').bold('Start autonomous mode'), description: 'HelixMind will continuously scan and fix issues' },
2104
+ { label: 'Cancel', description: 'Go back' },
2105
+ ], { title: chalk.hex('#ff6600').bold('Autonomous Mode'), cancelLabel: 'Cancel' });
2106
+ rl.resume();
2107
+ if (autoConfirm === 0) {
2108
+ await onAutonomous('start');
2109
+ }
2110
+ else {
2111
+ renderInfo('Autonomous mode cancelled.');
2112
+ }
2113
+ }
2114
+ break;
2115
+ }
2116
+ case '/stop':
2117
+ if (onAutonomous) {
2118
+ await onAutonomous('stop');
2119
+ }
2120
+ else {
2121
+ renderInfo('No autonomous mode running.');
2122
+ }
2123
+ break;
2124
+ case '/security':
2125
+ if (onAutonomous) {
2126
+ await onAutonomous('security');
2127
+ }
2128
+ break;
2129
+ case '/sessions':
2130
+ case '/session': {
2131
+ if (!sessionManager)
2132
+ break;
2133
+ const subCmd = parts[1]?.toLowerCase();
2134
+ if (subCmd === 'close' || subCmd === 'remove') {
2135
+ const targetId = parts[2];
2136
+ if (!targetId) {
2137
+ renderInfo('Usage: /session close <id>');
2138
+ break;
2139
+ }
2140
+ const removed = sessionManager.remove(targetId);
2141
+ if (removed) {
2142
+ renderInfo(`Session ${targetId} closed.`);
2143
+ }
2144
+ else {
2145
+ renderInfo(`Session "${targetId}" not found or is the main session.`);
2146
+ }
2147
+ }
2148
+ else if (subCmd === 'stop') {
2149
+ const targetId = parts[2];
2150
+ if (targetId) {
2151
+ const session = sessionManager.get(targetId);
2152
+ if (session && session.status === 'running') {
2153
+ session.abort();
2154
+ renderInfo(`Stopped: ${session.icon} ${session.name}`);
2155
+ }
2156
+ else {
2157
+ renderInfo(`Session "${targetId}" not found or not running.`);
2158
+ }
2159
+ }
2160
+ else {
2161
+ // Stop all background
2162
+ sessionManager.abortAll();
2163
+ renderInfo('All background sessions stopped.');
2164
+ }
2165
+ }
2166
+ else if (subCmd === 'switch') {
2167
+ const targetId = parts[2];
2168
+ if (targetId && sessionManager.switchTo(targetId)) {
2169
+ const s = sessionManager.active;
2170
+ renderInfo(`Switched to: ${s.icon} ${s.name}`);
2171
+ // Replay captured output
2172
+ if (s.output.length > 0) {
2173
+ process.stdout.write('\n' + chalk.dim('--- Session output ---') + '\n');
2174
+ for (const line of s.output.slice(-20)) {
2175
+ process.stdout.write(' ' + chalk.dim(line) + '\n');
2176
+ }
2177
+ process.stdout.write(chalk.dim('--- End ---') + '\n');
2178
+ }
2179
+ }
2180
+ else {
2181
+ renderInfo(`Session "${targetId || '?'}" not found.`);
2182
+ }
2183
+ }
2184
+ else {
2185
+ // Show session list
2186
+ process.stdout.write(renderSessionList(sessionManager.all, sessionManager.activeId));
2187
+ }
2188
+ break;
2189
+ }
2190
+ case '/local': {
2191
+ // Local LLM setup via Ollama
2192
+ rl.pause();
2193
+ const { isOllamaRunning, listOllamaModels, pullOllamaModel, formatModelSize, RECOMMENDED_MODELS } = await import('../providers/ollama.js');
2194
+ process.stdout.write('\n');
2195
+ const d = chalk.dim;
2196
+ const c = chalk.hex('#00d4ff');
2197
+ // Step 1: Check if Ollama is running
2198
+ const running = await isOllamaRunning();
2199
+ if (!running) {
2200
+ process.stdout.write(d('\u256D\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\u2500\u2500\u2500\u2500\u256E') + '\n' +
2201
+ d('\u2502 ') + chalk.yellow('\u26A0 Ollama not detected') + d(' \u2502') + '\n' +
2202
+ d('\u2502') + d(' \u2502') + '\n' +
2203
+ d('\u2502 ') + '1. Install: ' + c('https://ollama.com') + d(' \u2502') + '\n' +
2204
+ d('\u2502 ') + '2. Start: ' + c('ollama serve') + d(' \u2502') + '\n' +
2205
+ d('\u2502 ') + '3. Run: ' + c('/local') + ' again' + d(' \u2502') + '\n' +
2206
+ d('\u2502') + d(' \u2502') + '\n' +
2207
+ d('\u2570\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\u2500\u2500\u2500\u2500\u256F') + '\n\n');
2208
+ rl.resume();
2209
+ break;
2210
+ }
2211
+ // Step 2: Get installed models
2212
+ const installed = await listOllamaModels();
2213
+ const installedNames = new Set(installed.map(m => m.name));
2214
+ // Build menu: installed models first, then recommended to download
2215
+ const menuItems = [];
2216
+ const menuActions = [];
2217
+ if (installed.length > 0) {
2218
+ menuItems.push({ label: c.bold('Installed Models'), disabled: true });
2219
+ menuActions.push({ action: 'use', model: '' });
2220
+ for (const m of installed) {
2221
+ const size = formatModelSize(m.size);
2222
+ const quant = m.details?.quantization_level || '';
2223
+ const active = config.provider === 'ollama' && config.model === m.name;
2224
+ menuItems.push({
2225
+ label: theme.primary(m.name),
2226
+ description: `${size} ${quant}`,
2227
+ marker: active ? chalk.green('\u25C0 active') : undefined,
2228
+ });
2229
+ menuActions.push({ action: 'use', model: m.name });
2230
+ }
2231
+ }
2232
+ // Recommended models not yet installed
2233
+ const notInstalled = RECOMMENDED_MODELS.filter(r => !installedNames.has(r.name));
2234
+ if (notInstalled.length > 0) {
2235
+ menuItems.push({ label: '', disabled: true });
2236
+ menuActions.push({ action: 'pull', model: '' });
2237
+ menuItems.push({ label: chalk.hex('#00ff88').bold('Download New Model'), disabled: true });
2238
+ menuActions.push({ action: 'pull', model: '' });
2239
+ for (const r of notInstalled) {
2240
+ menuItems.push({
2241
+ label: chalk.hex('#00ff88')(r.name),
2242
+ description: `${r.size} \u2014 ${r.description}`,
2243
+ });
2244
+ menuActions.push({ action: 'pull', model: r.name });
2245
+ }
2246
+ }
2247
+ menuItems.push({ label: '', disabled: true });
2248
+ menuActions.push({ action: 'use', model: '' });
2249
+ menuItems.push({
2250
+ label: d(`Ollama running \u2713 | ${installed.length} model(s) installed`),
2251
+ disabled: true,
2252
+ });
2253
+ menuActions.push({ action: 'use', model: '' });
2254
+ const idx = await selectMenu(menuItems, {
2255
+ title: c.bold('\u{1F916} Local LLM Setup (Ollama)'),
2256
+ cancelLabel: 'Back',
2257
+ pageSize: 14,
2258
+ });
2259
+ rl.resume();
2260
+ if (idx < 0 || !menuActions[idx]?.model)
2261
+ break;
2262
+ const selected = menuActions[idx];
2263
+ if (selected.action === 'use') {
2264
+ // Switch to Ollama with selected model
2265
+ store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
2266
+ store.switchProvider('ollama', selected.model);
2267
+ config = store.getAll();
2268
+ try {
2269
+ const newProvider = createProvider('ollama', 'ollama', selected.model, 'http://localhost:11434/v1');
2270
+ onProviderSwitch?.(newProvider);
2271
+ renderInfo(`\u2705 Switched to local: ${chalk.bold(selected.model)}`);
2272
+ }
2273
+ catch (err) {
2274
+ renderError(`Failed to switch: ${err}`);
2275
+ }
2276
+ }
2277
+ else if (selected.action === 'pull') {
2278
+ // Download model
2279
+ renderInfo(`\u{2B07}\uFE0F Downloading ${chalk.bold(selected.model)}...`);
2280
+ process.stdout.write(d(' This may take a few minutes depending on model size.\n\n'));
2281
+ let lastPct = -1;
2282
+ const success = await pullOllamaModel(selected.model, (status, completed, total) => {
2283
+ if (completed && total && total > 0) {
2284
+ const pct = Math.round((completed / total) * 100);
2285
+ if (pct !== lastPct) {
2286
+ lastPct = pct;
2287
+ const bar = '\u2588'.repeat(Math.floor(pct / 5)) + '\u2591'.repeat(20 - Math.floor(pct / 5));
2288
+ process.stdout.write(`\r ${c(bar)} ${pct}% ${d(status || '')}`);
2289
+ }
2290
+ }
2291
+ else if (status) {
2292
+ process.stdout.write(`\r\x1b[K ${d(status)}`);
2293
+ }
2294
+ });
2295
+ process.stdout.write('\r\x1b[K');
2296
+ if (success) {
2297
+ renderInfo(`\u2705 Downloaded ${chalk.bold(selected.model)}`);
2298
+ // Auto-switch to the new model
2299
+ store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
2300
+ store.switchProvider('ollama', selected.model);
2301
+ config = store.getAll();
2302
+ try {
2303
+ const newProvider = createProvider('ollama', 'ollama', selected.model, 'http://localhost:11434/v1');
2304
+ onProviderSwitch?.(newProvider);
2305
+ renderInfo(`\u2705 Active model: ${chalk.bold(selected.model)}`);
2306
+ }
2307
+ catch (err) {
2308
+ renderError(`Model downloaded but failed to switch: ${err}`);
2309
+ }
2310
+ }
2311
+ else {
2312
+ renderError(`Failed to download ${selected.model}. Check Ollama logs.`);
2313
+ }
2314
+ }
2315
+ break;
2316
+ }
2317
+ default:
2318
+ renderError(`Unknown command: ${cmd}. Type /help for available commands.`);
2319
+ }
2320
+ }
2321
+ //# sourceMappingURL=chat.js.map