@calliopelabs/cli 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (538) hide show
  1. package/README.md +45 -644
  2. package/dist/acp.d.ts +78 -0
  3. package/dist/acp.d.ts.map +1 -0
  4. package/dist/acp.js +572 -0
  5. package/dist/acp.js.map +1 -0
  6. package/dist/auto-compressor.d.ts.map +1 -1
  7. package/dist/auto-compressor.js +9 -5
  8. package/dist/auto-compressor.js.map +1 -1
  9. package/dist/bin.d.ts +9 -5
  10. package/dist/bin.d.ts.map +1 -1
  11. package/dist/bin.js +108 -76
  12. package/dist/bin.js.map +1 -1
  13. package/dist/budget.d.ts +64 -0
  14. package/dist/budget.d.ts.map +1 -0
  15. package/dist/budget.js +157 -0
  16. package/dist/budget.js.map +1 -0
  17. package/dist/checkpoint.d.ts +63 -30
  18. package/dist/checkpoint.d.ts.map +1 -1
  19. package/dist/checkpoint.js +249 -150
  20. package/dist/checkpoint.js.map +1 -1
  21. package/dist/circuit-breaker/breaker.js.map +1 -1
  22. package/dist/config.d.ts +86 -81
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +237 -305
  25. package/dist/config.js.map +1 -1
  26. package/dist/cost.d.ts +114 -0
  27. package/dist/cost.d.ts.map +1 -0
  28. package/dist/cost.js +499 -0
  29. package/dist/cost.js.map +1 -0
  30. package/dist/diff.d.ts.map +1 -1
  31. package/dist/diff.js +53 -11
  32. package/dist/diff.js.map +1 -1
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +33 -6
  35. package/dist/errors.js.map +1 -1
  36. package/dist/files.js.map +1 -1
  37. package/dist/fleet.d.ts +30 -0
  38. package/dist/fleet.d.ts.map +1 -0
  39. package/dist/fleet.js +86 -0
  40. package/dist/fleet.js.map +1 -0
  41. package/dist/fuzzy-search.d.ts.map +1 -1
  42. package/dist/fuzzy-search.js +2 -1
  43. package/dist/fuzzy-search.js.map +1 -1
  44. package/dist/git-status.js.map +1 -1
  45. package/dist/headless.d.ts +1 -2
  46. package/dist/headless.d.ts.map +1 -1
  47. package/dist/headless.js +222 -26
  48. package/dist/headless.js.map +1 -1
  49. package/dist/hooks.d.ts +8 -2
  50. package/dist/hooks.d.ts.map +1 -1
  51. package/dist/hooks.js +97 -11
  52. package/dist/hooks.js.map +1 -1
  53. package/dist/hud/api.d.ts +3 -18
  54. package/dist/hud/api.d.ts.map +1 -1
  55. package/dist/hud/api.js +9 -352
  56. package/dist/hud/api.js.map +1 -1
  57. package/dist/hud/palettes.d.ts.map +1 -1
  58. package/dist/hud/palettes.js +0 -180
  59. package/dist/hud/palettes.js.map +1 -1
  60. package/dist/hud/skins.d.ts.map +1 -1
  61. package/dist/hud/skins.js +0 -263
  62. package/dist/hud/skins.js.map +1 -1
  63. package/dist/hud/types.d.ts +2 -5
  64. package/dist/hud/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +1 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -4
  68. package/dist/index.js.map +1 -1
  69. package/dist/iteration-ledger.d.ts.map +1 -1
  70. package/dist/iteration-ledger.js +3 -2
  71. package/dist/iteration-ledger.js.map +1 -1
  72. package/dist/local-model.d.ts +150 -0
  73. package/dist/local-model.d.ts.map +1 -0
  74. package/dist/local-model.js +408 -0
  75. package/dist/local-model.js.map +1 -0
  76. package/dist/markdown.d.ts.map +1 -1
  77. package/dist/markdown.js +32 -10
  78. package/dist/markdown.js.map +1 -1
  79. package/dist/mcp.d.ts +35 -5
  80. package/dist/mcp.d.ts.map +1 -1
  81. package/dist/mcp.js +190 -13
  82. package/dist/mcp.js.map +1 -1
  83. package/dist/memory.d.ts.map +1 -1
  84. package/dist/memory.js +6 -4
  85. package/dist/memory.js.map +1 -1
  86. package/dist/model-detection.d.ts +14 -1
  87. package/dist/model-detection.d.ts.map +1 -1
  88. package/dist/model-detection.js +308 -115
  89. package/dist/model-detection.js.map +1 -1
  90. package/dist/parallel-tools.d.ts +9 -1
  91. package/dist/parallel-tools.d.ts.map +1 -1
  92. package/dist/parallel-tools.js +6 -5
  93. package/dist/parallel-tools.js.map +1 -1
  94. package/dist/plugins.d.ts +68 -0
  95. package/dist/plugins.d.ts.map +1 -1
  96. package/dist/plugins.js +201 -3
  97. package/dist/plugins.js.map +1 -1
  98. package/dist/policy.d.ts +43 -0
  99. package/dist/policy.d.ts.map +1 -0
  100. package/dist/policy.js +139 -0
  101. package/dist/policy.js.map +1 -0
  102. package/dist/project-config.d.ts +1 -2
  103. package/dist/project-config.d.ts.map +1 -1
  104. package/dist/project-config.js +0 -3
  105. package/dist/project-config.js.map +1 -1
  106. package/dist/providers/anthropic.d.ts.map +1 -1
  107. package/dist/providers/anthropic.js +36 -2
  108. package/dist/providers/anthropic.js.map +1 -1
  109. package/dist/providers/bedrock.d.ts.map +1 -1
  110. package/dist/providers/bedrock.js +83 -22
  111. package/dist/providers/bedrock.js.map +1 -1
  112. package/dist/providers/compat.d.ts +21 -0
  113. package/dist/providers/compat.d.ts.map +1 -1
  114. package/dist/providers/compat.js +176 -6
  115. package/dist/providers/compat.js.map +1 -1
  116. package/dist/providers/google.js.map +1 -1
  117. package/dist/providers/index.d.ts +19 -4
  118. package/dist/providers/index.d.ts.map +1 -1
  119. package/dist/providers/index.js +62 -9
  120. package/dist/providers/index.js.map +1 -1
  121. package/dist/providers/ollama.d.ts +2 -2
  122. package/dist/providers/ollama.d.ts.map +1 -1
  123. package/dist/providers/ollama.js +26 -6
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.d.ts.map +1 -1
  126. package/dist/providers/openai.js +14 -10
  127. package/dist/providers/openai.js.map +1 -1
  128. package/dist/providers/types.d.ts +8 -0
  129. package/dist/providers/types.d.ts.map +1 -1
  130. package/dist/providers/types.js +19 -10
  131. package/dist/providers/types.js.map +1 -1
  132. package/dist/replay.d.ts +37 -0
  133. package/dist/replay.d.ts.map +1 -0
  134. package/dist/replay.js +168 -0
  135. package/dist/replay.js.map +1 -0
  136. package/dist/risk.d.ts.map +1 -1
  137. package/dist/risk.js +15 -5
  138. package/dist/risk.js.map +1 -1
  139. package/dist/router.d.ts +133 -0
  140. package/dist/router.d.ts.map +1 -0
  141. package/dist/{model-router.js → router.js} +336 -10
  142. package/dist/router.js.map +1 -0
  143. package/dist/runlog.d.ts +185 -0
  144. package/dist/runlog.d.ts.map +1 -0
  145. package/dist/runlog.js +398 -0
  146. package/dist/runlog.js.map +1 -0
  147. package/dist/{sandbox.d.ts → sandbox/docker.d.ts} +1 -1
  148. package/dist/sandbox/docker.d.ts.map +1 -0
  149. package/dist/{sandbox.js → sandbox/docker.js} +1 -1
  150. package/dist/sandbox/docker.js.map +1 -0
  151. package/dist/sandbox/index.d.ts +36 -0
  152. package/dist/sandbox/index.d.ts.map +1 -0
  153. package/dist/sandbox/index.js +62 -0
  154. package/dist/sandbox/index.js.map +1 -0
  155. package/dist/{sandbox-native.d.ts → sandbox/native.d.ts} +2 -1
  156. package/dist/sandbox/native.d.ts.map +1 -0
  157. package/dist/{sandbox-native.js → sandbox/native.js} +38 -6
  158. package/dist/sandbox/native.js.map +1 -0
  159. package/dist/scope.d.ts +10 -0
  160. package/dist/scope.d.ts.map +1 -1
  161. package/dist/scope.js +76 -16
  162. package/dist/scope.js.map +1 -1
  163. package/dist/scuttlebot/client.d.ts +83 -0
  164. package/dist/scuttlebot/client.d.ts.map +1 -0
  165. package/dist/scuttlebot/client.js +350 -0
  166. package/dist/scuttlebot/client.js.map +1 -0
  167. package/dist/scuttlebot/config.d.ts +28 -0
  168. package/dist/scuttlebot/config.d.ts.map +1 -0
  169. package/dist/scuttlebot/config.js +91 -0
  170. package/dist/scuttlebot/config.js.map +1 -0
  171. package/dist/scuttlebot/http-client.d.ts +63 -0
  172. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  173. package/dist/scuttlebot/http-client.js +124 -0
  174. package/dist/scuttlebot/http-client.js.map +1 -0
  175. package/dist/scuttlebot/index.d.ts +13 -0
  176. package/dist/scuttlebot/index.d.ts.map +1 -0
  177. package/dist/scuttlebot/index.js +10 -0
  178. package/dist/scuttlebot/index.js.map +1 -0
  179. package/dist/scuttlebot/irc-client.d.ts +124 -0
  180. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  181. package/dist/scuttlebot/irc-client.js +599 -0
  182. package/dist/scuttlebot/irc-client.js.map +1 -0
  183. package/dist/setup.d.ts.map +1 -1
  184. package/dist/setup.js +22 -54
  185. package/dist/setup.js.map +1 -1
  186. package/dist/skills.d.ts +58 -0
  187. package/dist/skills.d.ts.map +1 -1
  188. package/dist/skills.js +186 -16
  189. package/dist/skills.js.map +1 -1
  190. package/dist/storage.d.ts +0 -4
  191. package/dist/storage.d.ts.map +1 -1
  192. package/dist/storage.js +87 -8
  193. package/dist/storage.js.map +1 -1
  194. package/dist/streaming.d.ts +25 -0
  195. package/dist/streaming.d.ts.map +1 -1
  196. package/dist/streaming.js +59 -0
  197. package/dist/streaming.js.map +1 -1
  198. package/dist/styles.d.ts +6 -0
  199. package/dist/styles.d.ts.map +1 -1
  200. package/dist/styles.js +11 -0
  201. package/dist/styles.js.map +1 -1
  202. package/dist/summarization.js.map +1 -1
  203. package/dist/terminal-image.js +1 -1
  204. package/dist/terminal-image.js.map +1 -1
  205. package/dist/themes.d.ts +5 -4
  206. package/dist/themes.d.ts.map +1 -1
  207. package/dist/themes.js +35 -90
  208. package/dist/themes.js.map +1 -1
  209. package/dist/tools.d.ts +27 -3
  210. package/dist/tools.d.ts.map +1 -1
  211. package/dist/tools.js +352 -209
  212. package/dist/tools.js.map +1 -1
  213. package/dist/trust.d.ts +16 -3
  214. package/dist/trust.d.ts.map +1 -1
  215. package/dist/trust.js +23 -4
  216. package/dist/trust.js.map +1 -1
  217. package/dist/types.d.ts +4 -3
  218. package/dist/types.d.ts.map +1 -1
  219. package/dist/types.js +34 -44
  220. package/dist/types.js.map +1 -1
  221. package/dist/ui/agent.d.ts +4 -7
  222. package/dist/ui/agent.d.ts.map +1 -1
  223. package/dist/ui/agent.js +354 -73
  224. package/dist/ui/agent.js.map +1 -1
  225. package/dist/ui/animations.d.ts +37 -0
  226. package/dist/ui/animations.d.ts.map +1 -0
  227. package/dist/ui/animations.js +227 -0
  228. package/dist/ui/animations.js.map +1 -0
  229. package/dist/ui/chat-input-keys.d.ts +42 -0
  230. package/dist/ui/chat-input-keys.d.ts.map +1 -0
  231. package/dist/ui/chat-input-keys.js +360 -0
  232. package/dist/ui/chat-input-keys.js.map +1 -0
  233. package/dist/ui/chat-input.d.ts +5 -2
  234. package/dist/ui/chat-input.d.ts.map +1 -1
  235. package/dist/ui/chat-input.js +54 -308
  236. package/dist/ui/chat-input.js.map +1 -1
  237. package/dist/ui/commands.d.ts +15 -45
  238. package/dist/ui/commands.d.ts.map +1 -1
  239. package/dist/ui/commands.js +461 -2636
  240. package/dist/ui/commands.js.map +1 -1
  241. package/dist/ui/completions.d.ts +1 -0
  242. package/dist/ui/completions.d.ts.map +1 -1
  243. package/dist/ui/completions.js +36 -107
  244. package/dist/ui/completions.js.map +1 -1
  245. package/dist/ui/components.d.ts +14 -6
  246. package/dist/ui/components.d.ts.map +1 -1
  247. package/dist/ui/components.js +17 -226
  248. package/dist/ui/components.js.map +1 -1
  249. package/dist/ui/context.js +6 -6
  250. package/dist/ui/context.js.map +1 -1
  251. package/dist/ui/debug-log.d.ts +11 -0
  252. package/dist/ui/debug-log.d.ts.map +1 -0
  253. package/dist/ui/debug-log.js +21 -0
  254. package/dist/ui/debug-log.js.map +1 -0
  255. package/dist/ui/error-boundary.d.ts.map +1 -1
  256. package/dist/ui/error-boundary.js.map +1 -1
  257. package/dist/ui/frame.d.ts.map +1 -1
  258. package/dist/ui/frame.js +1 -3
  259. package/dist/ui/frame.js.map +1 -1
  260. package/dist/ui/index.d.ts +11 -3
  261. package/dist/ui/index.d.ts.map +1 -1
  262. package/dist/ui/index.js +46 -887
  263. package/dist/ui/index.js.map +1 -1
  264. package/dist/ui/input-utils.d.ts +20 -0
  265. package/dist/ui/input-utils.d.ts.map +1 -0
  266. package/dist/ui/input-utils.js +35 -0
  267. package/dist/ui/input-utils.js.map +1 -0
  268. package/dist/ui/messages.d.ts +8 -7
  269. package/dist/ui/messages.d.ts.map +1 -1
  270. package/dist/ui/messages.js +51 -35
  271. package/dist/ui/messages.js.map +1 -1
  272. package/dist/ui/modals/index.d.ts +11 -0
  273. package/dist/ui/modals/index.d.ts.map +1 -0
  274. package/dist/ui/modals/index.js +10 -0
  275. package/dist/ui/modals/index.js.map +1 -0
  276. package/dist/ui/{modals.d.ts → modals/prompts.d.ts} +5 -20
  277. package/dist/ui/modals/prompts.d.ts.map +1 -0
  278. package/dist/ui/{modals.js → modals/prompts.js} +14 -94
  279. package/dist/ui/modals/prompts.js.map +1 -0
  280. package/dist/ui/modals/selectors.d.ts +33 -0
  281. package/dist/ui/modals/selectors.d.ts.map +1 -0
  282. package/dist/ui/modals/selectors.js +111 -0
  283. package/dist/ui/modals/selectors.js.map +1 -0
  284. package/dist/ui/modals/setup.d.ts +17 -0
  285. package/dist/ui/modals/setup.d.ts.map +1 -0
  286. package/dist/ui/modals/setup.js +51 -0
  287. package/dist/ui/modals/setup.js.map +1 -0
  288. package/dist/ui/regions/input-region.d.ts +40 -0
  289. package/dist/ui/regions/input-region.d.ts.map +1 -0
  290. package/dist/ui/regions/input-region.js +116 -0
  291. package/dist/ui/regions/input-region.js.map +1 -0
  292. package/dist/ui/regions/modal-host.d.ts +41 -0
  293. package/dist/ui/regions/modal-host.d.ts.map +1 -0
  294. package/dist/ui/regions/modal-host.js +51 -0
  295. package/dist/ui/regions/modal-host.js.map +1 -0
  296. package/dist/ui/regions/render-probe.d.ts +22 -0
  297. package/dist/ui/regions/render-probe.d.ts.map +1 -0
  298. package/dist/ui/regions/render-probe.js +45 -0
  299. package/dist/ui/regions/render-probe.js.map +1 -0
  300. package/dist/ui/regions/static-scrollback.d.ts +39 -0
  301. package/dist/ui/regions/static-scrollback.d.ts.map +1 -0
  302. package/dist/ui/regions/static-scrollback.js +68 -0
  303. package/dist/ui/regions/static-scrollback.js.map +1 -0
  304. package/dist/ui/regions/status-region.d.ts +24 -0
  305. package/dist/ui/regions/status-region.d.ts.map +1 -0
  306. package/dist/ui/regions/status-region.js +17 -0
  307. package/dist/ui/regions/status-region.js.map +1 -0
  308. package/dist/ui/regions/transcript-region.d.ts +41 -0
  309. package/dist/ui/regions/transcript-region.d.ts.map +1 -0
  310. package/dist/ui/regions/transcript-region.js +52 -0
  311. package/dist/ui/regions/transcript-region.js.map +1 -0
  312. package/dist/ui/self-restart.d.ts +9 -0
  313. package/dist/ui/self-restart.d.ts.map +1 -0
  314. package/dist/ui/self-restart.js +24 -0
  315. package/dist/ui/self-restart.js.map +1 -0
  316. package/dist/ui/state/use-chat-controller.d.ts +23 -0
  317. package/dist/ui/state/use-chat-controller.d.ts.map +1 -0
  318. package/dist/ui/state/use-chat-controller.js +643 -0
  319. package/dist/ui/state/use-chat-controller.js.map +1 -0
  320. package/dist/ui/state/use-loop-state.d.ts +23 -0
  321. package/dist/ui/state/use-loop-state.d.ts.map +1 -0
  322. package/dist/ui/state/use-loop-state.js +39 -0
  323. package/dist/ui/state/use-loop-state.js.map +1 -0
  324. package/dist/ui/state/use-modal-state.d.ts +52 -0
  325. package/dist/ui/state/use-modal-state.d.ts.map +1 -0
  326. package/dist/ui/state/use-modal-state.js +44 -0
  327. package/dist/ui/state/use-modal-state.js.map +1 -0
  328. package/dist/ui/state/use-model-state.d.ts +29 -0
  329. package/dist/ui/state/use-model-state.d.ts.map +1 -0
  330. package/dist/ui/state/use-model-state.js +47 -0
  331. package/dist/ui/state/use-model-state.js.map +1 -0
  332. package/dist/ui/state/use-processing-state.d.ts +24 -0
  333. package/dist/ui/state/use-processing-state.d.ts.map +1 -0
  334. package/dist/ui/state/use-processing-state.js +46 -0
  335. package/dist/ui/state/use-processing-state.js.map +1 -0
  336. package/dist/ui/state/use-queue-state.d.ts +19 -0
  337. package/dist/ui/state/use-queue-state.d.ts.map +1 -0
  338. package/dist/ui/state/use-queue-state.js +30 -0
  339. package/dist/ui/state/use-queue-state.js.map +1 -0
  340. package/dist/ui/state/use-session-init.d.ts +22 -0
  341. package/dist/ui/state/use-session-init.d.ts.map +1 -0
  342. package/dist/ui/state/use-session-init.js +77 -0
  343. package/dist/ui/state/use-session-init.js.map +1 -0
  344. package/dist/ui/state/use-session-stats.d.ts +16 -0
  345. package/dist/ui/state/use-session-stats.d.ts.map +1 -0
  346. package/dist/ui/state/use-session-stats.js +18 -0
  347. package/dist/ui/state/use-session-stats.js.map +1 -0
  348. package/dist/ui/state/use-terminal-width.d.ts +8 -0
  349. package/dist/ui/state/use-terminal-width.d.ts.map +1 -0
  350. package/dist/ui/state/use-terminal-width.js +22 -0
  351. package/dist/ui/state/use-terminal-width.js.map +1 -0
  352. package/dist/ui/state/use-transcript-state.d.ts +21 -0
  353. package/dist/ui/state/use-transcript-state.d.ts.map +1 -0
  354. package/dist/ui/state/use-transcript-state.js +49 -0
  355. package/dist/ui/state/use-transcript-state.js.map +1 -0
  356. package/dist/ui/status-bar.d.ts +4 -1
  357. package/dist/ui/status-bar.d.ts.map +1 -1
  358. package/dist/ui/status-bar.js +13 -3
  359. package/dist/ui/status-bar.js.map +1 -1
  360. package/dist/ui/types.d.ts +3 -0
  361. package/dist/ui/types.d.ts.map +1 -1
  362. package/dist/version-check.d.ts.map +1 -1
  363. package/dist/version-check.js +10 -0
  364. package/dist/version-check.js.map +1 -1
  365. package/package.json +12 -8
  366. package/dist/agents/agent-config-loader.d.ts +0 -60
  367. package/dist/agents/agent-config-loader.d.ts.map +0 -1
  368. package/dist/agents/agent-config-loader.js +0 -402
  369. package/dist/agents/agent-config-loader.js.map +0 -1
  370. package/dist/agents/agent-config-presets.d.ts +0 -10
  371. package/dist/agents/agent-config-presets.d.ts.map +0 -1
  372. package/dist/agents/agent-config-presets.js +0 -940
  373. package/dist/agents/agent-config-presets.js.map +0 -1
  374. package/dist/agents/agent-config-types.d.ts +0 -145
  375. package/dist/agents/agent-config-types.d.ts.map +0 -1
  376. package/dist/agents/agent-config-types.js +0 -12
  377. package/dist/agents/agent-config-types.js.map +0 -1
  378. package/dist/agents/agent-detection.d.ts +0 -35
  379. package/dist/agents/agent-detection.d.ts.map +0 -1
  380. package/dist/agents/agent-detection.js +0 -110
  381. package/dist/agents/agent-detection.js.map +0 -1
  382. package/dist/agents/aggregator.d.ts +0 -19
  383. package/dist/agents/aggregator.d.ts.map +0 -1
  384. package/dist/agents/aggregator.js +0 -141
  385. package/dist/agents/aggregator.js.map +0 -1
  386. package/dist/agents/cli-backend.d.ts +0 -32
  387. package/dist/agents/cli-backend.d.ts.map +0 -1
  388. package/dist/agents/cli-backend.js +0 -271
  389. package/dist/agents/cli-backend.js.map +0 -1
  390. package/dist/agents/council-types.d.ts +0 -115
  391. package/dist/agents/council-types.d.ts.map +0 -1
  392. package/dist/agents/council-types.js +0 -81
  393. package/dist/agents/council-types.js.map +0 -1
  394. package/dist/agents/council.d.ts +0 -112
  395. package/dist/agents/council.d.ts.map +0 -1
  396. package/dist/agents/council.js +0 -722
  397. package/dist/agents/council.js.map +0 -1
  398. package/dist/agents/decomposer.d.ts +0 -33
  399. package/dist/agents/decomposer.d.ts.map +0 -1
  400. package/dist/agents/decomposer.js +0 -138
  401. package/dist/agents/decomposer.js.map +0 -1
  402. package/dist/agents/dynamic-tools.d.ts +0 -52
  403. package/dist/agents/dynamic-tools.d.ts.map +0 -1
  404. package/dist/agents/dynamic-tools.js +0 -395
  405. package/dist/agents/dynamic-tools.js.map +0 -1
  406. package/dist/agents/index.d.ts +0 -29
  407. package/dist/agents/index.d.ts.map +0 -1
  408. package/dist/agents/index.js +0 -29
  409. package/dist/agents/index.js.map +0 -1
  410. package/dist/agents/installer.d.ts +0 -39
  411. package/dist/agents/installer.d.ts.map +0 -1
  412. package/dist/agents/installer.js +0 -205
  413. package/dist/agents/installer.js.map +0 -1
  414. package/dist/agents/orchestrator.d.ts +0 -106
  415. package/dist/agents/orchestrator.d.ts.map +0 -1
  416. package/dist/agents/orchestrator.js +0 -363
  417. package/dist/agents/orchestrator.js.map +0 -1
  418. package/dist/agents/sdk-backend.d.ts +0 -63
  419. package/dist/agents/sdk-backend.d.ts.map +0 -1
  420. package/dist/agents/sdk-backend.js +0 -489
  421. package/dist/agents/sdk-backend.js.map +0 -1
  422. package/dist/agents/swarm-types.d.ts +0 -84
  423. package/dist/agents/swarm-types.d.ts.map +0 -1
  424. package/dist/agents/swarm-types.js +0 -20
  425. package/dist/agents/swarm-types.js.map +0 -1
  426. package/dist/agents/swarm.d.ts +0 -79
  427. package/dist/agents/swarm.d.ts.map +0 -1
  428. package/dist/agents/swarm.js +0 -375
  429. package/dist/agents/swarm.js.map +0 -1
  430. package/dist/agents/tools.d.ts +0 -26
  431. package/dist/agents/tools.d.ts.map +0 -1
  432. package/dist/agents/tools.js +0 -776
  433. package/dist/agents/tools.js.map +0 -1
  434. package/dist/agents/types.d.ts +0 -108
  435. package/dist/agents/types.d.ts.map +0 -1
  436. package/dist/agents/types.js +0 -43
  437. package/dist/agents/types.js.map +0 -1
  438. package/dist/api-server.d.ts +0 -36
  439. package/dist/api-server.d.ts.map +0 -1
  440. package/dist/api-server.js +0 -255
  441. package/dist/api-server.js.map +0 -1
  442. package/dist/auto-checkpoint.d.ts +0 -35
  443. package/dist/auto-checkpoint.d.ts.map +0 -1
  444. package/dist/auto-checkpoint.js +0 -143
  445. package/dist/auto-checkpoint.js.map +0 -1
  446. package/dist/background-jobs.d.ts +0 -45
  447. package/dist/background-jobs.d.ts.map +0 -1
  448. package/dist/background-jobs.js +0 -127
  449. package/dist/background-jobs.js.map +0 -1
  450. package/dist/branching.d.ts +0 -56
  451. package/dist/branching.d.ts.map +0 -1
  452. package/dist/branching.js +0 -211
  453. package/dist/branching.js.map +0 -1
  454. package/dist/cli/agent.d.ts +0 -9
  455. package/dist/cli/agent.d.ts.map +0 -1
  456. package/dist/cli/agent.js +0 -314
  457. package/dist/cli/agent.js.map +0 -1
  458. package/dist/cli/commands.d.ts +0 -12
  459. package/dist/cli/commands.d.ts.map +0 -1
  460. package/dist/cli/commands.js +0 -943
  461. package/dist/cli/commands.js.map +0 -1
  462. package/dist/cli/index.d.ts +0 -8
  463. package/dist/cli/index.d.ts.map +0 -1
  464. package/dist/cli/index.js +0 -237
  465. package/dist/cli/index.js.map +0 -1
  466. package/dist/cli/types.d.ts +0 -33
  467. package/dist/cli/types.d.ts.map +0 -1
  468. package/dist/cli/types.js +0 -21
  469. package/dist/cli/types.js.map +0 -1
  470. package/dist/companions.d.ts +0 -54
  471. package/dist/companions.d.ts.map +0 -1
  472. package/dist/companions.js +0 -440
  473. package/dist/companions.js.map +0 -1
  474. package/dist/completion.d.ts +0 -75
  475. package/dist/completion.d.ts.map +0 -1
  476. package/dist/completion.js +0 -234
  477. package/dist/completion.js.map +0 -1
  478. package/dist/file-watcher.d.ts +0 -91
  479. package/dist/file-watcher.d.ts.map +0 -1
  480. package/dist/file-watcher.js +0 -269
  481. package/dist/file-watcher.js.map +0 -1
  482. package/dist/hud/theme-packs/api.d.ts +0 -51
  483. package/dist/hud/theme-packs/api.d.ts.map +0 -1
  484. package/dist/hud/theme-packs/api.js +0 -145
  485. package/dist/hud/theme-packs/api.js.map +0 -1
  486. package/dist/hud/theme-packs/index.d.ts +0 -18
  487. package/dist/hud/theme-packs/index.d.ts.map +0 -1
  488. package/dist/hud/theme-packs/index.js +0 -38
  489. package/dist/hud/theme-packs/index.js.map +0 -1
  490. package/dist/hud/theme-packs/types.d.ts +0 -29
  491. package/dist/hud/theme-packs/types.d.ts.map +0 -1
  492. package/dist/hud/theme-packs/types.js +0 -9
  493. package/dist/hud/theme-packs/types.js.map +0 -1
  494. package/dist/idle-eviction.d.ts +0 -34
  495. package/dist/idle-eviction.d.ts.map +0 -1
  496. package/dist/idle-eviction.js +0 -78
  497. package/dist/idle-eviction.js.map +0 -1
  498. package/dist/keyboard.d.ts +0 -57
  499. package/dist/keyboard.d.ts.map +0 -1
  500. package/dist/keyboard.js +0 -265
  501. package/dist/keyboard.js.map +0 -1
  502. package/dist/model-router.d.ts +0 -67
  503. package/dist/model-router.d.ts.map +0 -1
  504. package/dist/model-router.js.map +0 -1
  505. package/dist/providers/openai-compat-shims.d.ts +0 -31
  506. package/dist/providers/openai-compat-shims.d.ts.map +0 -1
  507. package/dist/providers/openai-compat-shims.js +0 -179
  508. package/dist/providers/openai-compat-shims.js.map +0 -1
  509. package/dist/sandbox-native.d.ts.map +0 -1
  510. package/dist/sandbox-native.js.map +0 -1
  511. package/dist/sandbox.d.ts.map +0 -1
  512. package/dist/sandbox.js.map +0 -1
  513. package/dist/session-timeout.d.ts +0 -31
  514. package/dist/session-timeout.d.ts.map +0 -1
  515. package/dist/session-timeout.js +0 -100
  516. package/dist/session-timeout.js.map +0 -1
  517. package/dist/smart-router.d.ts +0 -73
  518. package/dist/smart-router.d.ts.map +0 -1
  519. package/dist/smart-router.js +0 -332
  520. package/dist/smart-router.js.map +0 -1
  521. package/dist/terminal-recording.d.ts +0 -55
  522. package/dist/terminal-recording.d.ts.map +0 -1
  523. package/dist/terminal-recording.js +0 -182
  524. package/dist/terminal-recording.js.map +0 -1
  525. package/dist/tmux.d.ts +0 -35
  526. package/dist/tmux.d.ts.map +0 -1
  527. package/dist/tmux.js +0 -106
  528. package/dist/tmux.js.map +0 -1
  529. package/dist/ui/modals.d.ts.map +0 -1
  530. package/dist/ui/modals.js.map +0 -1
  531. package/dist/ui/pack-picker.d.ts +0 -12
  532. package/dist/ui/pack-picker.d.ts.map +0 -1
  533. package/dist/ui/pack-picker.js +0 -101
  534. package/dist/ui/pack-picker.js.map +0 -1
  535. package/dist/ui/theme-picker.d.ts +0 -24
  536. package/dist/ui/theme-picker.d.ts.map +0 -1
  537. package/dist/ui/theme-picker.js +0 -190
  538. package/dist/ui/theme-picker.js.map +0 -1
@@ -1,100 +1,97 @@
1
1
  /**
2
2
  * UI Module - Command Handler
3
3
  *
4
- * Handles all slash commands (/help, /mode, /provider, etc.)
5
- * Extracted from TerminalChat using a CommandContext state bag.
4
+ * Handles the slash commands exposed by the TUI. The surface is intentionally
5
+ * small: see COMMAND_NAMES for the canonical list. Legacy/verbose commands have
6
+ * been folded into subcommands of the survivors (e.g. /models -> /model list,
7
+ * /add-dir -> /scope add, /set -> /config set, /breakloop -> /loop stop).
6
8
  */
7
9
  import * as fs from 'fs';
8
10
  import * as path from 'path';
9
11
  import * as config from '../config.js';
10
12
  import { selectProvider, getAvailableProviders } from '../providers/index.js';
11
- import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG } from '../types.js';
12
- import { getVersion, getLatestVersion } from '../version-check.js';
13
- import { getAvailableModels } from '../model-detection.js';
13
+ import { MODE_CONFIG } from '../types.js';
14
+ import { getSystemPromptForProvider } from '../local-model.js';
15
+ import { getAvailableModels, getModelContextLimit } from '../model-detection.js';
14
16
  import * as storage from '../storage.js';
15
17
  import * as mcp from '../mcp.js';
16
18
  import * as skills from '../skills.js';
17
- import * as modelRouter from '../model-router.js';
18
19
  import * as summarization from '../summarization.js';
19
20
  import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetScope } from '../scope.js';
20
- import { getAgentStatusReport, swarmManager, councilManager, COUNCIL_TEMPLATES, getInstallReport, installItem, installAllMissing, listAgentDefs, listTeamDefs, getAgent, getTeam, scaffoldAgentsDir, saveAgentDef, getAvailableExecutors } from '../agents/index.js';
21
- import { smartRoute, getDefaultSmartRoutingConfig, detectTaskType } from '../smart-router.js';
22
- import { getCurrentSkin, getCurrentPalette, applySkin, applyPalette, listSkins, listPalettes } from '../hud/api.js';
23
- import { getCurrentCompanion, applyCompanion, listCompanions, getMoodText } from '../companions.js';
24
- import { createJob, runJob, cancelJob, getJob, formatJob, formatJobsList, clearFinishedJobs } from '../background-jobs.js';
25
- import { listRecordings, loadRecording, formatRecording, deleteRecording } from '../terminal-recording.js';
26
- import { startApiServer, stopApiServer, isApiServerRunning } from '../api-server.js';
27
- import { getTerminalImageInfo, getImageModeLabel, renderSkinBanner, renderTransition } from '../terminal-image.js';
28
- import { applyThemePack, listThemePacks, getCurrentPack, getCompanionMode, setCompanionMode, getThemePack } from '../hud/theme-packs/api.js';
29
- import { getModelContextLimit } from '../model-detection.js';
21
+ import { fleetConfigured, fleetActive, fleetStatus, fleetEnable, fleetDisable, fleetPostMessage } from '../fleet.js';
22
+ import { getTerminalImageInfo, getImageModeLabel } from '../terminal-image.js';
30
23
  import { resetContextWarnings } from './context.js';
31
24
  import * as memory from '../memory.js';
25
+ import { getBudgetCaps, hasBudgetCaps, loadProjectSpend } from '../budget.js';
32
26
  import { resolveIterationLimit, formatIterationLimit, isFiniteIterationLimit, } from '../iteration-limit.js';
27
+ // ============================================================================
28
+ // Command registry
29
+ // ============================================================================
30
+ /**
31
+ * Canonical list of every slash-command label handled by the switch in
32
+ * handleCommand(). Tests assert this stays in lockstep with the switch and a
33
+ * superset of the completion roots. `/quit` is the sole alias (of `/exit`);
34
+ * `/fleet` is flag-gated in completions but always handled here.
35
+ */
36
+ export const COMMAND_NAMES = [
37
+ '/help',
38
+ '/status',
39
+ '/clear',
40
+ '/exit',
41
+ '/quit',
42
+ '/model',
43
+ '/provider',
44
+ '/mode',
45
+ '/undo',
46
+ '/export',
47
+ '/resume',
48
+ '/compact',
49
+ '/scope',
50
+ '/memory',
51
+ '/mcp',
52
+ '/skills',
53
+ '/config',
54
+ '/setup',
55
+ '/trust',
56
+ '/cost',
57
+ '/loop',
58
+ '/restore',
59
+ '/debug',
60
+ '/fleet',
61
+ ];
33
62
  // Builds the full system prompt including memory context (project + global).
34
- // dir should be the project directory for the active/resumed session.
35
- function buildFullSystemPrompt(persona, dir) {
36
- const base = getSystemPrompt(persona);
63
+ // dir should be the project directory for the active/resumed session; provider
64
+ // selects the compact (local) vs full (cloud) base prompt (feature 5).
65
+ function buildFullSystemPrompt(dir, provider) {
66
+ const base = getSystemPromptForProvider(provider);
37
67
  const mem = memory.buildMemoryContext(dir);
38
68
  return mem.trim() ? base + '\n\n--- Project Context ---\n' + mem : base;
39
69
  }
40
70
  function getActiveProjectDir(ctx) {
41
71
  return ctx.sessionRef.current?.projectPath ?? process.cwd();
42
72
  }
43
- function formatLedgerDuration(durationMs) {
44
- if (durationMs < 1000)
45
- return `${durationMs}ms`;
46
- if (durationMs < 60_000)
47
- return `${(durationMs / 1000).toFixed(1)}s`;
48
- return `${(durationMs / 60_000).toFixed(1)}m`;
49
- }
50
73
  function formatSessionLogLimit(limit) {
51
74
  return limit > 0 ? String(limit) : 'unlimited';
52
75
  }
53
- function formatLedgerRun(run) {
54
- const iterationCount = Math.max(0, (run.entryCountAtEnd ?? run.entryCountAtStart) - run.entryCountAtStart);
55
- const parts = [`${run.kind}`, `[${run.status}]`];
56
- if (iterationCount > 0) {
57
- parts.push(`${iterationCount} iteration${iterationCount === 1 ? '' : 's'}`);
58
- }
59
- if (run.maxIterations != null && Number.isFinite(run.maxIterations)) {
60
- parts.push(`max ${run.maxIterations}`);
61
- }
62
- else if (run.maxIterations === null) {
63
- parts.push('unlimited');
76
+ /**
77
+ * Apply a `budget.*` config key. `off`/`none`/empty clears the cap. Throws on an
78
+ * invalid number so the caller's try/catch surfaces the message.
79
+ */
80
+ function applyBudgetKey(field, value) {
81
+ const budget = { ...(config.get('budget') ?? {}) };
82
+ if (value === 'off' || value === 'none' || value === '') {
83
+ delete budget[field];
84
+ config.set('budget', budget);
85
+ return `✓ ${field} cleared`;
64
86
  }
65
- const prompt = run.prompt.length > 90 ? `${run.prompt.slice(0, 90)}...` : run.prompt;
66
- let line = `${parts.join(' ')} — ${prompt}`;
67
- if (run.errorSummary) {
68
- line += ` (${run.errorSummary})`;
87
+ const num = Number(value);
88
+ if (isNaN(num) || num < 0) {
89
+ throw new Error(`${field} must be a non-negative number (or 'off' to clear)`);
69
90
  }
70
- return line;
71
- }
72
- function watchAsyncLedgerRun(ledger, kind, prompt, getStatus) {
73
- if (!ledger)
74
- return;
75
- const runId = ledger.startRun(kind, prompt);
76
- void (async () => {
77
- for (;;) {
78
- const current = getStatus();
79
- if (!current) {
80
- ledger.finishRun(runId, 'failed', { errorSummary: 'Run state no longer available' });
81
- return;
82
- }
83
- if (current.status === 'completed') {
84
- ledger.finishRun(runId, 'completed');
85
- return;
86
- }
87
- if (current.status === 'failed') {
88
- ledger.finishRun(runId, 'failed', { errorSummary: current.error });
89
- return;
90
- }
91
- if (current.status === 'cancelled') {
92
- ledger.finishRun(runId, 'cancelled', { errorSummary: current.error || 'Cancelled' });
93
- return;
94
- }
95
- await new Promise(resolve => setTimeout(resolve, 500));
96
- }
97
- })();
91
+ budget[field] = field === 'maxTokensPerRun' ? Math.floor(num) : num;
92
+ config.set('budget', budget);
93
+ const unit = field === 'maxTokensPerRun' ? ' tokens' : ' USD';
94
+ return `✓ ${field} set to ${budget[field]}${unit}`;
98
95
  }
99
96
  // ============================================================================
100
97
  // handleCommand
@@ -103,108 +100,79 @@ export async function handleCommand(cmd, ctx) {
103
100
  const parts = cmd.split(/\s+/);
104
101
  const command = parts[0].toLowerCase();
105
102
  switch (command) {
106
- case '/help':
107
- case '/h':
108
- ctx.addMessage('system', `--- Model & Routing ---
109
- /provider [name] - Switch AI provider (/p)
110
- /model [name] - Switch model (/m)
111
- /models - List available models
112
- /mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
113
- /persona [name] - Switch personality
114
- /route [on|off] - Auto model routing (/autoroute)
115
- /smart [on|off|cost|test] - Cross-provider smart routing
116
- /breaker [status|resume] - Circuit breaker control (/cb)
117
-
118
- --- Conversation ---
119
- /edit - Edit and resend last message
120
- /undo / /redo - Undo/redo (up to 10 steps)
121
- /copy - Copy last response to clipboard
122
- /export [file.md] - Export conversation to markdown
123
- /branch [new|switch] - Conversation branches
124
- /clear - Clear conversation (/c)
103
+ case '/help': {
104
+ let help = `Session
105
+ /help Show this help
106
+ /status Provider, model, tokens, terminal, fleet
107
+ /clear Clear the conversation
108
+ /exit Exit Calliope (alias /quit)
125
109
 
126
- --- Session & State ---
127
- /session [list|info|fork|save] - Session management (/sessions)
128
- /log [summary|tail|failures|reset] - Iteration/run log
129
- /resume [sessionId] - Resume session (restores full context)
130
- /checkpoint [list|clear] - File checkpoints (/cp)
131
- /restore <path> [index] - Restore file from checkpoint
132
- /bookmark [name|list|goto] - Bookmarks (/bm)
133
- /queue [show|clear|flush] - Message queue (/q)
110
+ Model & Mode
111
+ /model [name|list] Switch model, or list/pick available models
112
+ /provider [name|list] Switch provider, or list providers
113
+ /mode [plan|hybrid|work] Switch mode (Shift+Tab to cycle)
134
114
 
135
- --- Context & Scope ---
136
- /scope [details|reset] - File access scope (/dirs)
137
- /add-dir / /remove-dir - Manage scope directories
138
- /context [load|summary] - Context management
139
- /summarize - Compact context to save tokens
140
- /trust [add|remove|list] - Project trust registry
141
- /find <pattern> - Fuzzy file search
142
- /search <query> - Search conversation
115
+ Conversation
116
+ /undo Undo the last change (up to 10 steps)
117
+ /export [file.md] Export conversation to markdown
118
+ /resume [sessionId] Resume a saved session (restores full context)
119
+ /compact [status] Compress conversation context; status shows a summary
143
120
 
144
- --- Tasks & Planning ---
145
- /todo [add|done|list] - Manage TODOs
146
- /plans [list|view] - View plan history
147
- /template [save|use|del] - Prompt templates (/t)
148
- /plan / /work - Quick mode switch
149
- /approve [notes] - Approve pending plan & execute
121
+ Workspace
122
+ /scope [add|remove <dir>] File-access scope (also details|reset)
123
+ /memory [init|add|show] Project memory (CALLIOPE.md)
124
+ /trust [add|remove|list] Project trust registry
125
+ /restore [<path> [index]] List checkpoints, or restore a file
150
126
 
151
- --- Appearance ---
152
- /skin [name] - Switch HUD skin (${(await import('../hud/api.js')).listSkins().length}+ available)
153
- /palette [name] - Switch color palette
154
- /companion [name] - Switch companion personality
155
- /pack [name] - Apply theme pack (skin+palette+companion)
156
- /theme [name] - Color themes
157
- /layout [name] - UI layout (classic/split/etc)
158
- /density [normal|compact] - Display density
159
- /collapse [tools|all|off] - Tool output visibility
160
- /intensity [1-5] - Immersion level
161
- /emoji [on|off] - Toggle emoji
162
- /banner - Show skin banner art
127
+ Extend
128
+ /mcp [add|remove|tools] MCP servers
129
+ /skills [add|remove] Agent skills
163
130
 
164
- --- Tools & Integration ---
165
- /mcp [add|remove|tools] - MCP servers
166
- /skills [add|remove] - Agent skills
167
- /memory [init|add|show] - Project memory (CALLIOPE.md)
168
- /project [init|show|run] - Project config (.calliope)
169
- /hooks [list|add] - Pre/post tool hooks
170
- /profile [name|save|del] - Switch/save/delete profiles
131
+ System
132
+ /config [set <key> <value>] Show or change settings (maxIterations, sessionLogLimit, diffStyle, theme)
133
+ /setup Reconfigure Calliope
134
+ /cost Cost tracking summary
135
+ /loop ["prompt"|stop] Iterative agent loop
136
+ /debug [on|off] Debug state/logging`;
137
+ if (fleetConfigured()) {
138
+ help += `
171
139
 
172
- --- Multi-Agent ---
173
- /agents - Sub-agent status (--agents mode)
174
- /swarm [start|coord|status] - Agent swarms & coordination
175
- /loop [prompt] - Iterative agent loop (default: unlimited)
176
- /cancel-loop - Stop running loop (/stop, /breakloop)
177
-
178
- --- System ---
179
- /status - Show status (/s)
180
- /config - Show config
181
- /set <key> <value> - Set config variable
182
- /cost - Cost tracking summary
183
- /confirm [on|off] - Toggle risky op confirmation
184
- /debug [on|off] - Debug state/logging
185
- /unstick - Emergency reset
186
- /upgrade - Check for updates
187
- /keys or /? - Keyboard shortcuts
188
- /exit - Exit (/quit)
140
+ Fleet
141
+ /fleet [message|disable] Multi-agent fleet coordination`;
142
+ }
143
+ help += `
189
144
 
190
- File references: @filename, ./path, /absolute/path
191
- Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.agtermEnabled ? '\nAgents: ON' : ''}`);
145
+ File references: @filename, ./path, /absolute/path`;
146
+ ctx.addMessage('system', help);
192
147
  break;
148
+ }
193
149
  case '/provider':
194
- case '/providers':
195
- case '/p':
196
- if (parts[1]) {
197
- const p = parts[1].toLowerCase();
198
- ctx.setProvider(p);
199
- ctx.addMessage('system', `Provider: ${selectProvider(p)}`);
150
+ if (parts[1] && parts[1] !== 'list') {
151
+ const requested = parts[1].toLowerCase();
152
+ const available = getAvailableProviders();
153
+ if (!available.includes(requested)) {
154
+ ctx.addMessage('error', `Provider "${requested}" is not configured. Run /provider (no args) for an interactive picker with setup.`);
155
+ break;
156
+ }
157
+ ctx.setProvider(requested);
158
+ // Explicit switches persist as the new defaults; a stale model from
159
+ // the previous provider must not leak into this one (#233).
160
+ config.set('defaultProvider', requested);
161
+ config.unset('defaultModel');
162
+ ctx.addMessage('system', `Provider: ${selectProvider(requested)} (saved as default)`);
163
+ }
164
+ else if (parts[1] === 'list') {
165
+ ctx.addMessage('system', `Provider: ${ctx.actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
166
+ }
167
+ else if (ctx.openProviderPicker) {
168
+ ctx.openProviderPicker();
200
169
  }
201
170
  else {
202
171
  ctx.addMessage('system', `Provider: ${ctx.actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
203
172
  }
204
173
  break;
205
174
  case '/model':
206
- case '/m':
207
- if (parts[1]) {
175
+ if (parts[1] && parts[1] !== 'list') {
208
176
  const newModel = parts[1];
209
177
  const oldModel = ctx.model || ctx.actualModel;
210
178
  // Check context compatibility before switching (#26)
@@ -213,20 +181,22 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
213
181
  const currentTokens = ctx.estimateContextTokens();
214
182
  const newPct = Math.round((currentTokens / newLimit) * 100);
215
183
  ctx.setModel(newModel);
184
+ config.set('defaultModel', newModel);
216
185
  ctx.setContextTokens(currentTokens);
217
186
  let switchWarning = '';
218
187
  if (newPct > 80) {
219
- switchWarning = `\n⚠️ Context at ${newPct}% of new model limit (${Math.round(currentTokens / 1000)}K/${Math.round(newLimit / 1000)}K). Consider /summarize compact.`;
188
+ switchWarning = `\n⚠️ Context at ${newPct}% of new model limit (${Math.round(currentTokens / 1000)}K/${Math.round(newLimit / 1000)}K). Consider /compact.`;
220
189
  }
221
190
  else if (newLimit < oldLimit) {
222
191
  switchWarning = `\n📉 Context window: ${Math.round(oldLimit / 1000)}K → ${Math.round(newLimit / 1000)}K (${newPct}% used)`;
223
192
  }
224
- ctx.addMessage('system', `Model: ${oldModel} → ${newModel}${switchWarning}`);
193
+ ctx.addMessage('system', `Model: ${oldModel} → ${newModel} (saved as default)${switchWarning}`);
225
194
  }
226
195
  else {
196
+ // No arg or `list`: open the model picker (absorbs the old /models listing)
227
197
  ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
228
198
  try {
229
- const models = await getAvailableModels(ctx.actualProvider);
199
+ const models = await getAvailableModels(ctx.actualProvider, { throwOnError: true });
230
200
  if (models.length > 0) {
231
201
  ctx.setAvailableModels(models);
232
202
  ctx.setModalMode('model');
@@ -240,22 +210,6 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
240
210
  }
241
211
  }
242
212
  break;
243
- case '/models':
244
- ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
245
- try {
246
- const models = await getAvailableModels(ctx.actualProvider);
247
- if (models.length > 0) {
248
- ctx.setAvailableModels(models);
249
- ctx.setModalMode('model');
250
- }
251
- else {
252
- ctx.addMessage('error', `No models found for ${ctx.actualProvider} — API key may be invalid`);
253
- }
254
- }
255
- catch (e) {
256
- ctx.addMessage('error', `Failed to fetch models for ${ctx.actualProvider}: ${e instanceof Error ? e.message : String(e)}. Check your API key.`);
257
- }
258
- break;
259
213
  case '/mode':
260
214
  if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
261
215
  const m = parts[1];
@@ -267,64 +221,13 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
267
221
  ctx.addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (\u{1F4CB}), hybrid (\u{1F504}), work (\u{1F527})\nUse Shift+Tab to cycle`);
268
222
  }
269
223
  break;
270
- case '/persona':
271
- if (parts[1] && ['calliope', 'muse', 'minimal'].includes(parts[1])) {
272
- const p = parts[1];
273
- ctx.setPersona(p);
274
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(p, getActiveProjectDir(ctx)) }];
275
- ctx.addMessage('system', `Persona: ${p}`);
276
- }
277
- else {
278
- ctx.addMessage('system', `Persona: ${ctx.persona} | Options: calliope, muse, minimal`);
279
- }
280
- break;
281
224
  case '/clear':
282
- case '/c':
283
225
  ctx.setMessages([]);
284
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
226
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(getActiveProjectDir(ctx), ctx.actualProvider) }];
285
227
  ctx.ledger?.reset();
286
228
  ctx.setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
287
229
  resetContextWarnings(); // Reset context warning state
288
230
  break;
289
- case '/copy': {
290
- // Copy last assistant response to clipboard
291
- const lastAssistant = [...ctx.messages].reverse().find(m => m.type === 'assistant');
292
- if (lastAssistant) {
293
- try {
294
- const { execSync } = await import('child_process');
295
- // Try different clipboard commands based on platform
296
- const content = lastAssistant.content;
297
- if (process.platform === 'darwin') {
298
- execSync('pbcopy', { input: content });
299
- }
300
- else if (process.platform === 'win32') {
301
- execSync('clip', { input: content });
302
- }
303
- else {
304
- // Linux - try xclip, xsel, or wl-copy
305
- try {
306
- execSync('xclip -selection clipboard', { input: content });
307
- }
308
- catch {
309
- try {
310
- execSync('xsel --clipboard --input', { input: content });
311
- }
312
- catch {
313
- execSync('wl-copy', { input: content });
314
- }
315
- }
316
- }
317
- ctx.addMessage('system', '\u2713 Copied to clipboard');
318
- }
319
- catch (e) {
320
- ctx.addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
321
- }
322
- }
323
- else {
324
- ctx.addMessage('system', 'No assistant message to copy');
325
- }
326
- break;
327
- }
328
231
  case '/export': {
329
232
  // Export conversation to markdown
330
233
  const filename = parts[1] || `calliope-export-${Date.now()}.md`;
@@ -353,20 +256,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
353
256
  }
354
257
  const filepath = path.resolve(process.cwd(), filename);
355
258
  fsModule.writeFileSync(filepath, markdown);
356
- ctx.addMessage('system', `\u2713 Exported to ${filename}`);
357
- break;
358
- }
359
- case '/edit': {
360
- // Edit last user message
361
- const lastUserIdx = [...ctx.messages].reverse().findIndex(m => m.type === 'user');
362
- if (lastUserIdx >= 0) {
363
- const lastUser = ctx.messages[ctx.messages.length - 1 - lastUserIdx];
364
- ctx.setInput(lastUser.content);
365
- ctx.addMessage('system', 'Edit the message above and press Enter to resend');
366
- }
367
- else {
368
- ctx.addMessage('system', 'No user message to edit');
369
- }
259
+ ctx.addMessage('system', `✓ Exported to ${filename}`);
370
260
  break;
371
261
  }
372
262
  case '/undo': {
@@ -385,651 +275,251 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
385
275
  ctx.setMessages(prevState.messages);
386
276
  ctx.llmMessages.current = prevState.llmMessages;
387
277
  ctx.setContextTokens(ctx.estimateContextTokens());
388
- ctx.addMessage('system', `\u2713 Undone (${ctx.undoStack.current.length} more available)`);
389
- break;
390
- }
391
- case '/redo': {
392
- if (ctx.redoStack.current.length === 0) {
393
- ctx.addMessage('system', 'Nothing to redo.');
394
- break;
395
- }
396
- // Save current state to undo stack
397
- ctx.undoStack.current.push({
398
- messages: [...ctx.messages],
399
- llmMessages: [...ctx.llmMessages.current],
400
- timestamp: new Date(),
401
- });
402
- // Restore redo state
403
- const redoState = ctx.redoStack.current.pop();
404
- ctx.setMessages(redoState.messages);
405
- ctx.llmMessages.current = redoState.llmMessages;
406
- ctx.setContextTokens(ctx.estimateContextTokens());
407
- ctx.addMessage('system', `\u2713 Redone (${ctx.redoStack.current.length} more available)`);
278
+ ctx.addMessage('system', `✓ Undone (${ctx.undoStack.current.length} more available)`);
408
279
  break;
409
280
  }
410
- case '/status':
411
- case '/s': {
281
+ case '/status': {
412
282
  const imgInfo = getTerminalImageInfo();
413
- ctx.addMessage('system', `${ctx.actualProvider}:${ctx.actualModel} | ${ctx.stats.messageCount} msgs | ${ctx.stats.inputTokens + ctx.stats.outputTokens} tokens | terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''} ${imgInfo.width}cols`);
414
- break;
415
- }
416
- case '/config':
417
- ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}\nsessionLogLimit: ${formatSessionLogLimit(config.get('sessionLogLimit'))} (set > 0 to cap)`);
418
- break;
419
- case '/agents': {
420
- if (!ctx.agtermEnabled) {
421
- ctx.addMessage('system', 'Agents mode not enabled. Start with --agents flag to unlock multi-agent features.');
283
+ let statusMsg = `${ctx.actualProvider}:${ctx.actualModel} | ${ctx.stats.messageCount} msgs | ${ctx.stats.inputTokens + ctx.stats.outputTokens} tokens | terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''} ${imgInfo.width}cols`;
284
+ // Add fleet status if active
285
+ const fleetSt = fleetStatus();
286
+ if (fleetSt) {
287
+ statusMsg += `\nFleet: active (${fleetSt.nick}) | irc:${fleetSt.config?.ircAddr} | #${fleetSt.config?.channel}`;
288
+ }
289
+ // Show budget state when any cap is configured.
290
+ const caps = getBudgetCaps();
291
+ if (hasBudgetCaps(caps)) {
292
+ const parts = [];
293
+ if (typeof caps.maxCostPerRun === 'number')
294
+ parts.push(`run<=$${caps.maxCostPerRun}`);
295
+ if (typeof caps.maxTokensPerRun === 'number')
296
+ parts.push(`run<=${caps.maxTokensPerRun}tok`);
297
+ if (typeof caps.maxCostPerProject === 'number') {
298
+ const spent = loadProjectSpend(getActiveProjectDir(ctx)).spentUsd;
299
+ parts.push(`project $${spent.toFixed(4)}/$${caps.maxCostPerProject}`);
300
+ }
301
+ statusMsg += `\nBudget: ${parts.join(' | ')}`;
302
+ }
303
+ ctx.addMessage('system', statusMsg);
304
+ break;
305
+ }
306
+ case '/fleet': {
307
+ const subCmd = parts[1];
308
+ if (subCmd === 'enable') {
309
+ if (fleetActive()) {
310
+ ctx.addMessage('system', 'Fleet mode is already active.');
311
+ break;
312
+ }
313
+ const sessionId = ctx.sessionRef.current?.id || 'default';
314
+ const cwd = getActiveProjectDir(ctx);
315
+ fleetEnable(sessionId, cwd).then((enabled) => {
316
+ if (enabled) {
317
+ const status = fleetStatus();
318
+ let msg = '✓ Fleet mode enabled\n';
319
+ msg += ` Nick: ${status?.nick}\n`;
320
+ msg += ` IRC: ${status?.config?.ircAddr}\n`;
321
+ msg += ` Channel: #${status?.config?.channel}`;
322
+ ctx.addMessage('system', msg);
323
+ ctx.startFleetPolling();
324
+ }
325
+ else {
326
+ ctx.addMessage('system', 'Failed to enable fleet mode — check relay config (~/.config/scuttlebot-relay.env, .scuttlebot.yaml).');
327
+ }
328
+ }).catch((err) => {
329
+ ctx.addMessage('system', `Failed to enable fleet mode: ${err instanceof Error ? err.message : String(err)}`);
330
+ });
422
331
  break;
423
332
  }
424
- const sub = parts[1];
425
- if (!sub) {
426
- ctx.addMessage('system', getAgentStatusReport());
427
- }
428
- else if (sub === 'defs' || sub === 'list') {
429
- const defs = listAgentDefs(process.cwd());
430
- if (defs.length === 0) {
431
- ctx.addMessage('system', 'No agent definitions loaded. Run /agents init to create examples.');
432
- }
433
- else {
434
- const lines = defs.map(d => {
435
- const src = d._source || 'unknown';
436
- return ` ${d.name} — ${d.engine}/${d.provider || 'auto'}/${d.model || 'default'} [${src}]${d.description ? `\n ${d.description}` : ''}`;
437
- });
438
- ctx.addMessage('system', `Agent Definitions (${defs.length}):\n${lines.join('\n')}`);
333
+ if (subCmd === 'disable') {
334
+ if (!fleetConfigured() && !fleetActive()) {
335
+ ctx.addMessage('system', 'Fleet mode is not enabled.');
336
+ break;
439
337
  }
338
+ fleetDisable().then(() => {
339
+ ctx.addMessage('system', 'Fleet mode disabled.');
340
+ }).catch((err) => {
341
+ ctx.addMessage('system', `Error disabling fleet mode: ${err instanceof Error ? err.message : String(err)}`);
342
+ });
343
+ break;
440
344
  }
441
- else if (sub === 'teams') {
442
- const teams = listTeamDefs(process.cwd());
443
- if (teams.length === 0) {
444
- ctx.addMessage('system', 'No team definitions loaded. Run /agents init to create examples.');
445
- }
446
- else {
447
- const lines = teams.map(t => {
448
- const src = t._source || 'unknown';
449
- return ` ${t.name} — ${t.mode}, ${t.members.length} members [${src}]${t.description ? `\n ${t.description}` : ''}`;
450
- });
451
- ctx.addMessage('system', `Team Definitions (${teams.length}):\n${lines.join('\n')}`);
452
- }
345
+ if (!fleetActive()) {
346
+ ctx.addMessage('system', 'Fleet mode is off.\n\nRun: /fleet enable\n\nRelay config is read from ~/.config/scuttlebot-relay.env and .scuttlebot.yaml.\nDocs: docs/fleet.md');
347
+ break;
453
348
  }
454
- else if (sub === 'init') {
455
- const result = scaffoldAgentsDir(process.cwd());
456
- if (result.created.length > 0) {
457
- ctx.addMessage('system', `Created .calliope/agents/ with examples:\n${result.created.map(f => ` ${f}`).join('\n')}`);
458
- }
459
- else {
460
- ctx.addMessage('system', '.calliope/agents/ already exists.');
461
- }
349
+ if (subCmd) {
350
+ const message = parts.slice(1).join(' ');
351
+ fleetPostMessage(message);
352
+ ctx.addMessage('system', 'Message posted to the fleet channel.');
353
+ break;
462
354
  }
463
- else if (sub === 'show') {
464
- const name = parts[2];
465
- if (!name) {
466
- ctx.addMessage('system', 'Usage: /agents show <name>');
355
+ const status = fleetStatus();
356
+ let statusText = 'Fleet Status\n────────────────────\n';
357
+ statusText += `Nick: ${status?.nick}\n`;
358
+ statusText += `IRC: ${status?.config?.ircAddr}\n`;
359
+ statusText += `Channel: #${status?.config?.channel}\n`;
360
+ statusText += `Connected: ${status?.connected ? 'yes' : 'no'}`;
361
+ statusText += '\n\nCommands:\n /fleet <message> Post to the fleet channel\n /fleet disable Disable fleet mode';
362
+ ctx.addMessage('system', statusText);
363
+ break;
364
+ }
365
+ case '/config': {
366
+ if (parts[1] === 'set') {
367
+ // /config set <key> <value> (absorbs the old /set)
368
+ const key = parts[2];
369
+ const value = parts.slice(3).join(' ');
370
+ if (!key || !value) {
371
+ ctx.addMessage('system', `Usage: /config set <key> <value>
372
+ Available keys:
373
+ maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
374
+ sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
375
+ collapseTools <bool> - Auto-collapse tool output
376
+ toolDisplayLimit <number> - Tools shown expanded (0 = all)
377
+ diffStyle <inline|unified|side-by-side> - Diff display style
378
+ sandboxMode <auto|native|docker|off> - Code execution sandbox
379
+ routing.enabled <bool> - Smart model routing
380
+ routing.costSensitivity <0-1> - Cost vs quality (0 = best, 1 = cheapest)
381
+ theme <dark|light|no-color> - Color theme
382
+ budget.maxCostPerRun <usd|off> - Halt a run at this spend (0/off = no cap)
383
+ budget.maxTokensPerRun <n|off> - Halt a run at this token count
384
+ budget.maxCostPerProject <usd|off> - Halt when project spend reaches this
385
+ audit.enabled <bool> - Audit run log (on by default)
386
+ policy.command <path|off> - Pre-tool policy hook script`);
387
+ break;
467
388
  }
468
- else {
469
- const agentDef = getAgent(name, process.cwd());
470
- const teamDef = getTeam(name, process.cwd());
471
- if (agentDef) {
472
- const lines = [
473
- `Agent: ${agentDef.name}`,
474
- agentDef.description ? `Description: ${agentDef.description}` : '',
475
- `Engine: ${agentDef.engine}`,
476
- `Provider: ${agentDef.provider || 'auto'}`,
477
- `Model: ${agentDef.model || 'default'}`,
478
- agentDef.role ? `Role: ${agentDef.role}` : '',
479
- agentDef.weight !== undefined ? `Weight: ${agentDef.weight}` : '',
480
- agentDef.instructions ? `Instructions:\n ${agentDef.instructions.split('\n').join('\n ')}` : '',
481
- `Source: ${agentDef._source || 'unknown'}`,
482
- agentDef._filePath ? `File: ${agentDef._filePath}` : '',
483
- ].filter(Boolean);
484
- ctx.addMessage('system', lines.join('\n'));
389
+ try {
390
+ if (key === 'maxIterations') {
391
+ const num = parseInt(value, 10);
392
+ if (isNaN(num) || num < 0 || num > 1000000) {
393
+ ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
394
+ break;
395
+ }
396
+ config.set('maxIterations', num);
397
+ ctx.addMessage('system', `✓ maxIterations set to ${formatIterationLimit(resolveIterationLimit(num))}`);
485
398
  }
486
- else if (teamDef) {
487
- const lines = [
488
- `Team: ${teamDef.name}`,
489
- teamDef.description ? `Description: ${teamDef.description}` : '',
490
- `Mode: ${teamDef.mode}`,
491
- `Members (${teamDef.members.length}):`,
492
- ...teamDef.members.map(m => ` ${m.name} — ${m.engine}/${m.provider || 'auto'}/${m.model || 'default'} [${m.role || 'no role'}] w=${m.weight}`),
493
- ].filter(Boolean);
494
- ctx.addMessage('system', lines.join('\n'));
399
+ else if (key === 'sessionLogLimit') {
400
+ const num = parseInt(value, 10);
401
+ if (isNaN(num) || num < 0 || num > 100000) {
402
+ ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
403
+ break;
404
+ }
405
+ config.set('sessionLogLimit', num);
406
+ ctx.ledger?.setRetentionLimit(num);
407
+ ctx.addMessage('system', `✓ sessionLogLimit set to ${num === 0 ? 'unlimited (set > 0 to cap)' : num}`);
495
408
  }
496
- else {
497
- ctx.addMessage('system', `No agent or team found with name '${name}'.`);
409
+ else if (key === 'collapseTools') {
410
+ const bool = value === 'true';
411
+ config.set('collapseTools', bool);
412
+ ctx.addMessage('system', `✓ collapseTools set to ${bool}`);
498
413
  }
499
- }
500
- }
501
- else {
502
- ctx.addMessage('system', `Unknown subcommand: ${sub}\nUsage: /agents [defs|teams|init|show <name>]`);
503
- }
504
- break;
505
- }
506
- case '/install-agents': {
507
- // /install-agents [name] — install missing agent CLIs and SDK backends
508
- const target = parts[1];
509
- if (target) {
510
- ctx.addMessage('system', `Installing ${target}...`);
511
- const result = installItem(target);
512
- ctx.addMessage('system', result.success
513
- ? `✓ ${target} installed successfully.`
514
- : `✗ ${result.output}`);
515
- }
516
- else if (parts.includes('--all')) {
517
- ctx.addMessage('system', 'Installing all missing agents and SDK backends...');
518
- const result = await installAllMissing();
519
- const lines = [];
520
- if (result.installed.length > 0) {
521
- lines.push(`✓ Installed: ${result.installed.join(', ')}`);
522
- }
523
- if (result.failed.length > 0) {
524
- lines.push(`✗ Failed:\n ${result.failed.join('\n ')}`);
525
- }
526
- if (result.installed.length === 0 && result.failed.length === 0) {
527
- lines.push('Everything is already installed!');
528
- }
529
- ctx.addMessage('system', lines.join('\n'));
530
- }
531
- else {
532
- const report = await getInstallReport();
533
- ctx.addMessage('system', `${report}\n\nUsage:\n /install-agents <name> Install a specific agent or SDK\n /install-agents --all Install everything missing`);
534
- }
535
- break;
536
- }
537
- case '/build-agent': {
538
- // Interactive agent definition builder
539
- // Parse inline args: /build-agent [name] [--engine X] [--provider X] [--model X]
540
- const agentName = parts[1] && !parts[1].startsWith('--') ? parts[1] : undefined;
541
- const engineArg = parts.includes('--engine') ? parts[parts.indexOf('--engine') + 1] : undefined;
542
- const providerArg = parts.includes('--provider') ? parts[parts.indexOf('--provider') + 1] : undefined;
543
- const modelArg = parts.includes('--model') ? parts[parts.indexOf('--model') + 1] : undefined;
544
- const roleArg = parts.includes('--role') ? parts[parts.indexOf('--role') + 1] : undefined;
545
- if (!agentName) {
546
- // Interactive wizard — show guided step-by-step builder
547
- const executors = await getAvailableExecutors();
548
- const providers = config.getConfiguredProviders();
549
- // Engine descriptions
550
- const engineDescriptions = {
551
- 'cli': 'Built-in CLI agent loop (works with all providers, most flexible)',
552
- 'claude-sdk': 'Anthropic Claude SDK (native tool use, streaming, best for Anthropic models)',
553
- 'openai-sdk': 'OpenAI SDK (function calling, JSON mode, best for OpenAI models)',
554
- 'google-adk': 'Google ADK (grounding, search, best for Gemini models)',
555
- };
556
- // Provider-to-engine recommendations
557
- const providerEngineMap = {
558
- anthropic: 'claude-sdk',
559
- openai: 'openai-sdk',
560
- google: 'google-adk',
561
- together: 'cli',
562
- openrouter: 'cli',
563
- groq: 'cli',
564
- fireworks: 'cli',
565
- mistral: 'cli',
566
- ollama: 'cli',
567
- bedrock: 'cli',
568
- };
569
- // Common roles
570
- const roles = [
571
- { name: 'coder', desc: 'Writes and refactors code' },
572
- { name: 'reviewer', desc: 'Reviews code for bugs, style, and security' },
573
- { name: 'architect', desc: 'Designs system architecture and APIs' },
574
- { name: 'researcher', desc: 'Gathers information and summarizes findings' },
575
- { name: 'qa-engineer', desc: 'Writes tests and validates correctness' },
576
- { name: 'devops', desc: 'Infrastructure, CI/CD, and deployment' },
577
- { name: 'writer', desc: 'Documentation, READMEs, and technical writing' },
578
- { name: 'analyst', desc: 'Data analysis and insights' },
579
- ];
580
- const lines = [];
581
- lines.push('=== Build Agent Wizard ===');
582
- lines.push('');
583
- // Step 1: Engine
584
- lines.push('STEP 1: Choose an engine');
585
- lines.push('The engine determines how the agent executes (SDK backend or CLI loop).');
586
- lines.push('');
587
- for (const eng of ['cli', 'claude-sdk', 'openai-sdk', 'google-adk']) {
588
- const available = executors.includes(eng);
589
- const marker = available ? '[installed]' : '[not installed]';
590
- lines.push(` ${eng} ${marker}`);
591
- lines.push(` ${engineDescriptions[eng] || ''}`);
592
- }
593
- lines.push('');
594
- if (executors.length === 1) {
595
- lines.push(' Recommendation: Only "cli" is available. Install SDK packages for more options.');
596
- lines.push(' Run /install-agents to see what can be installed.');
597
- }
598
- else {
599
- lines.push(` Available on this system: ${executors.join(', ')}`);
600
- }
601
- lines.push('');
602
- // Step 2: Provider
603
- lines.push('STEP 2: Choose a provider');
604
- lines.push('The provider determines which API your agent calls.');
605
- lines.push('');
606
- if (providers.length === 0) {
607
- lines.push(' No providers configured! Run calliope --setup to add API keys.');
608
- }
609
- else {
610
- for (const p of providers) {
611
- const recEngine = providerEngineMap[p] || 'cli';
612
- const model = DEFAULT_MODELS[p] || 'auto';
613
- lines.push(` ${p} (best with --engine ${recEngine}, default model: ${model})`);
414
+ else if (key === 'toolDisplayLimit') {
415
+ const num = parseInt(value, 10);
416
+ if (isNaN(num) || num < 0 || num > 100) {
417
+ ctx.addMessage('error', 'toolDisplayLimit must be 0-100 (0 = all expanded)');
418
+ break;
419
+ }
420
+ config.set('toolDisplayLimit', num);
421
+ ctx.addMessage('system', `✓ toolDisplayLimit set to ${num}`);
614
422
  }
615
- }
616
- lines.push('');
617
- // Step 3: Model
618
- lines.push('STEP 3: Choose a model');
619
- lines.push('Each provider has a default model. Override with --model if needed.');
620
- lines.push('');
621
- if (providers.length > 0) {
622
- for (const p of providers) {
623
- lines.push(` ${p}: ${DEFAULT_MODELS[p] || 'auto'}`);
423
+ else if (key === 'diffStyle') {
424
+ if (value !== 'inline' && value !== 'unified' && value !== 'side-by-side') {
425
+ ctx.addMessage('error', 'diffStyle must be inline, unified, or side-by-side');
426
+ break;
427
+ }
428
+ config.set('diffStyle', value);
429
+ ctx.addMessage('system', `✓ diffStyle set to ${value}`);
624
430
  }
625
- lines.push('');
626
- lines.push(' Tip: Run /models to see all available models for your providers.');
627
- }
628
- lines.push('');
629
- // Step 4: Role
630
- lines.push('STEP 4: Set a role');
631
- lines.push('The role labels what this agent specializes in. Common roles:');
632
- lines.push('');
633
- for (const r of roles) {
634
- lines.push(` ${r.name.padEnd(14)} ${r.desc}`);
635
- }
636
- lines.push('');
637
- lines.push(' You can also use any custom role name.');
638
- lines.push('');
639
- // Step 5: Instructions
640
- lines.push('STEP 5: Write instructions (after creation)');
641
- lines.push('After creating the agent, edit its YAML file to add detailed instructions.');
642
- lines.push('');
643
- lines.push(' Template:');
644
- lines.push(' instructions: |');
645
- lines.push(' You are a specialized {role} agent.');
646
- lines.push(' Your focus areas: ...');
647
- lines.push(' When reviewing code: ...');
648
- lines.push(' Output format: ...');
649
- lines.push('');
650
- // Final command
651
- lines.push('--- Ready to build? ---');
652
- lines.push('');
653
- if (providers.includes('anthropic') && executors.includes('claude-sdk')) {
654
- lines.push(' /build-agent my-agent --engine claude-sdk --provider anthropic --role coder');
655
- }
656
- else if (providers.includes('openai') && executors.includes('openai-sdk')) {
657
- lines.push(' /build-agent my-agent --engine openai-sdk --provider openai --role coder');
658
- }
659
- else if (providers.length > 0) {
660
- const firstProvider = providers[0];
661
- lines.push(` /build-agent my-agent --engine cli --provider ${firstProvider} --role coder`);
662
- }
663
- else {
664
- lines.push(' /build-agent my-agent --engine cli --provider anthropic --role coder');
665
- }
666
- lines.push('');
667
- lines.push('Add --model <model> to override the default model for the chosen provider.');
668
- ctx.addMessage('system', lines.join('\n'));
669
- }
670
- else if (!engineArg && !providerArg && !modelArg && !roleArg) {
671
- // Name provided but no flags — show a concise personalized wizard
672
- const executors = await getAvailableExecutors();
673
- const providers = config.getConfiguredProviders();
674
- const providerEngineMap = {
675
- anthropic: 'claude-sdk', openai: 'openai-sdk', google: 'google-adk',
676
- together: 'cli', openrouter: 'cli', groq: 'cli', fireworks: 'cli',
677
- mistral: 'cli', ollama: 'cli', bedrock: 'cli',
678
- };
679
- const lines = [];
680
- lines.push(`=== Building agent: ${agentName} ===`);
681
- lines.push('');
682
- lines.push('Choose your configuration:');
683
- lines.push('');
684
- // Show ready-to-run commands for each configured provider
685
- let optionNum = 0;
686
- for (const p of providers) {
687
- const recEngine = providerEngineMap[p] || 'cli';
688
- const engineAvailable = executors.includes(recEngine);
689
- const engine = engineAvailable ? recEngine : 'cli';
690
- const model = DEFAULT_MODELS[p] || 'auto';
691
- optionNum++;
692
- lines.push(` Option ${optionNum}: ${p}`);
693
- lines.push(` /build-agent ${agentName} --engine ${engine} --provider ${p} --model ${model} --role coder`);
694
- lines.push('');
695
- }
696
- if (optionNum === 0) {
697
- lines.push(' No providers configured. Run calliope --setup to add API keys.');
698
- lines.push(` Or build with defaults: /build-agent ${agentName} --engine cli --provider anthropic --role coder`);
699
- }
700
- lines.push('Common roles: coder, reviewer, architect, researcher, qa-engineer, devops, writer');
701
- lines.push('');
702
- lines.push('Pick an option above and change --role to match your needs.');
703
- lines.push(`After creation, customize instructions in .calliope/agents/${agentName}.yaml`);
704
- ctx.addMessage('system', lines.join('\n'));
705
- }
706
- else {
707
- // Build and save agent definition (flags provided)
708
- const newDef = {
709
- name: agentName,
710
- engine: engineArg || 'cli',
711
- provider: providerArg,
712
- model: modelArg,
713
- role: roleArg,
714
- weight: 1.0,
715
- instructions: `You are a specialized agent (${roleArg || agentName}). Complete assigned tasks thoroughly.`,
716
- limits: { timeout: 600000 },
717
- };
718
- const filePath = saveAgentDef(process.cwd(), newDef);
719
- ctx.addMessage('system', `Agent '${agentName}' created: ${filePath}\n\nEdit the file to customize instructions and settings.\nUse with: spawn_agent agentDef="${agentName}"`);
720
- }
721
- break;
722
- }
723
- case '/build-team': {
724
- // Interactive team builder
725
- const teamNameArg = parts[1] && !parts[1].startsWith('--') ? parts[1] : undefined;
726
- const modeArg = parts.includes('--mode') ? parts[parts.indexOf('--mode') + 1] : undefined;
727
- if (!teamNameArg) {
728
- // Interactive wizard — guided team builder
729
- const teams = listTeamDefs(process.cwd());
730
- const agents = listAgentDefs(process.cwd());
731
- const lines = [];
732
- lines.push('=== Build Team Wizard ===');
733
- lines.push('');
734
- // Step 1: Available agents
735
- lines.push('STEP 1: Review your available agents');
736
- lines.push('Teams are composed of agent definitions. Here is what you have:');
737
- lines.push('');
738
- if (agents.length === 0) {
739
- lines.push(' No agents defined yet!');
740
- lines.push(' Create agents first: /build-agent (run without args for the wizard)');
741
- lines.push('');
742
- }
743
- else {
744
- for (const a of agents) {
745
- const src = a._source === 'builtin' ? ' (built-in)' : '';
746
- lines.push(` ${a.name.padEnd(20)} engine: ${a.engine.padEnd(12)} provider: ${(a.provider || 'auto').padEnd(12)} role: ${a.role || 'any'}${src}`);
431
+ else if (key === 'sandboxMode') {
432
+ if (value !== 'auto' && value !== 'native' && value !== 'docker' && value !== 'off') {
433
+ ctx.addMessage('error', 'sandboxMode must be auto, native, docker, or off');
434
+ break;
435
+ }
436
+ config.set('sandboxMode', value);
437
+ ctx.addMessage('system', `✓ sandboxMode set to ${value}`);
747
438
  }
748
- lines.push('');
749
- lines.push(` Total: ${agents.length} agents available`);
750
- if (agents.length < 2) {
751
- lines.push(' You need at least 2 agents for a team. Create more with /build-agent.');
439
+ else if (key === 'routing.enabled') {
440
+ const bool = value === 'true';
441
+ const routing = config.get('routing') ?? { enabled: false, costSensitivity: 0.3 };
442
+ config.set('routing', { ...routing, enabled: bool });
443
+ ctx.addMessage('system', `✓ routing.enabled set to ${bool}`);
752
444
  }
753
- lines.push('');
754
- }
755
- // Step 2: Choose mode
756
- lines.push('STEP 2: Choose a coordination mode');
757
- lines.push('The mode determines how agents work together:');
758
- lines.push('');
759
- lines.push(' competitive');
760
- lines.push(' All agents tackle the same task independently.');
761
- lines.push(' The best response wins (scored by quality/relevance).');
762
- lines.push(' Best for: code review, creative alternatives, getting multiple perspectives.');
763
- lines.push('');
764
- lines.push(' collaborative');
765
- lines.push(' Agents work in a pipeline, each building on the previous output.');
766
- lines.push(' First agent plans, next implements, last reviews.');
767
- lines.push(' Best for: refactoring, multi-step tasks, plan-implement-review workflows.');
768
- lines.push('');
769
- lines.push(' consensus');
770
- lines.push(' All agents must agree before a result is accepted.');
771
- lines.push(' Disagreements trigger additional rounds of discussion.');
772
- lines.push(' Best for: security audits, critical decisions, high-stakes analysis.');
773
- lines.push('');
774
- lines.push(' overseer');
775
- lines.push(' A lead agent decomposes the task and delegates to workers.');
776
- lines.push(' The overseer reviews and merges all results.');
777
- lines.push(' Best for: complex projects, research, large feature implementation.');
778
- lines.push('');
779
- // Step 3: Strategy recommendations
780
- lines.push('STEP 3: Pick a strategy based on your task');
781
- lines.push('');
782
- lines.push(' Task Type Mode Recommended Agents');
783
- lines.push(' ─────────────────────────────────────────────────────────');
784
- lines.push(' Code review competitive code-reviewer, qa-engineer');
785
- lines.push(' Refactoring collaborative architect, coder, reviewer');
786
- lines.push(' Security audit consensus security-analyst, code-reviewer, qa-engineer');
787
- lines.push(' Research overseer researcher, analyst, writer');
788
- lines.push(' Feature development overseer architect, coder, qa-engineer, reviewer');
789
- lines.push(' Documentation collaborative researcher, writer');
790
- lines.push('');
791
- // Existing teams
792
- if (teams.length > 0) {
793
- lines.push('Existing teams:');
794
- for (const t of teams) {
795
- lines.push(` ${t.name.padEnd(20)} mode: ${t.mode.padEnd(14)} members: ${t.members.length}`);
445
+ else if (key === 'routing.costSensitivity') {
446
+ const num = Number(value);
447
+ if (isNaN(num) || num < 0 || num > 1) {
448
+ ctx.addMessage('error', 'routing.costSensitivity must be between 0 and 1');
449
+ break;
450
+ }
451
+ const routing = config.get('routing') ?? { enabled: false, costSensitivity: 0.3 };
452
+ config.set('routing', { ...routing, costSensitivity: num });
453
+ ctx.addMessage('system', `✓ routing.costSensitivity set to ${num}`);
796
454
  }
797
- lines.push('');
798
- }
799
- // Final command
800
- lines.push('--- Ready to build? ---');
801
- lines.push('');
802
- if (agents.length >= 2) {
803
- const sampleAgents = agents.slice(0, 3).map(a => a.name).join(', ');
804
- lines.push(` /build-team my-team --mode collaborative`);
805
- lines.push('');
806
- lines.push(` This will auto-compose a team from your available agents (${sampleAgents}${agents.length > 3 ? ', ...' : ''}).`);
807
- lines.push(' After creation, edit the YAML to fine-tune members and settings.');
808
- }
809
- else {
810
- lines.push(' First, create at least 2 agents:');
811
- lines.push(' /build-agent planner --engine cli --provider anthropic --role architect');
812
- lines.push(' /build-agent coder --engine cli --provider anthropic --role coder');
813
- lines.push('');
814
- lines.push(' Then build a team:');
815
- lines.push(' /build-team my-team --mode collaborative');
816
- }
817
- lines.push('');
818
- lines.push('After creation, use the team with:');
819
- lines.push(' /swarm --team <name> <prompt>');
820
- lines.push(' spawn_agent team="<name>" prompt="..."');
821
- ctx.addMessage('system', lines.join('\n'));
822
- }
823
- else {
824
- // Create a team with available agents
825
- const availableAgents = listAgentDefs(process.cwd());
826
- const mode = (modeArg || 'collaborative');
827
- // Auto-compose a reasonable team from available agent definitions
828
- const memberAgents = availableAgents
829
- .filter(a => a._source !== 'builtin' || ['code-reviewer', 'architect', 'qa-engineer', 'researcher'].includes(a.name))
830
- .slice(0, 4);
831
- if (memberAgents.length < 2) {
832
- ctx.addMessage('system', `Need at least 2 agent definitions to build a team.\nCreate agents first with /build-agent <name>, then compose them with /build-team.`);
833
- }
834
- else {
835
- const { saveTeamDef } = await import('../agents/index.js');
836
- const teamDef = {
837
- name: teamNameArg,
838
- description: `${teamNameArg} team`,
839
- mode,
840
- members: memberAgents.map(a => ({ agent: a.name, role: a.role, weight: a.weight })),
841
- swarm: { strategy: 'parallel', aggregation: 'structured', maxWorkers: 3 },
842
- council: { maxRounds: 2, consensusThreshold: 0.67 },
843
- };
844
- const filePath = saveTeamDef(process.cwd(), teamDef);
845
- ctx.addMessage('system', `Team '${teamNameArg}' created: ${filePath}
846
-
847
- Members:
848
- ${memberAgents.map(a => ` ${a.name} [${a.role || 'any'}]`).join('\n')}
849
-
850
- Mode: ${mode}
851
-
852
- Use with:
853
- /swarm --team ${teamNameArg} <prompt>
854
- /swarm coord --team ${teamNameArg} <prompt>
855
- spawn_agent team="${teamNameArg}" prompt="..."
856
-
857
- Edit the YAML to customize members, strategy, and coordination settings.`);
858
- }
859
- }
860
- break;
861
- }
862
- case '/set': {
863
- // /set <key> <value>
864
- const key = parts[1];
865
- const value = parts.slice(2).join(' ');
866
- if (!key || !value) {
867
- ctx.addMessage('system', `Usage: /set <key> <value>
868
- Available keys:
869
- maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
870
- sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
871
- persona <name> - calliope, muse, minimal
872
- fancyOutput <bool> - true/false`);
873
- break;
874
- }
875
- try {
876
- if (key === 'maxIterations') {
877
- const num = parseInt(value, 10);
878
- if (isNaN(num) || num < 0 || num > 1000000) {
879
- ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
880
- break;
455
+ else if (key === 'theme') {
456
+ const themes = await import('../themes.js');
457
+ if (themes.setCurrentTheme(value)) {
458
+ themes.clearThemeCache();
459
+ ctx.addMessage('system', `✓ theme set to ${value}`);
460
+ }
461
+ else {
462
+ ctx.addMessage('error', `Theme not found: ${value}. Options: dark, light, no-color`);
463
+ }
881
464
  }
882
- config.set('maxIterations', num);
883
- ctx.addMessage('system', `\u2713 maxIterations set to ${formatIterationLimit(resolveIterationLimit(num))}`);
884
- }
885
- else if (key === 'sessionLogLimit') {
886
- const num = parseInt(value, 10);
887
- if (isNaN(num) || num < 0 || num > 100000) {
888
- ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
889
- break;
465
+ else if (key === 'budget.maxCostPerRun') {
466
+ ctx.addMessage('system', applyBudgetKey('maxCostPerRun', value));
890
467
  }
891
- config.set('sessionLogLimit', num);
892
- ctx.ledger?.setRetentionLimit(num);
893
- ctx.addMessage('system', `\u2713 sessionLogLimit set to ${num === 0 ? 'unlimited (set > 0 to cap)' : num}`);
894
- }
895
- else if (key === 'persona') {
896
- if (!['calliope', 'muse', 'minimal'].includes(value)) {
897
- ctx.addMessage('error', 'persona must be: calliope, muse, or minimal');
898
- break;
468
+ else if (key === 'budget.maxTokensPerRun') {
469
+ ctx.addMessage('system', applyBudgetKey('maxTokensPerRun', value));
470
+ }
471
+ else if (key === 'budget.maxCostPerProject') {
472
+ ctx.addMessage('system', applyBudgetKey('maxCostPerProject', value));
473
+ }
474
+ else if (key === 'audit.enabled') {
475
+ const bool = value === 'true';
476
+ config.set('audit', { ...(config.get('audit') ?? {}), enabled: bool });
477
+ ctx.addMessage('system', `✓ audit.enabled set to ${bool}`);
478
+ }
479
+ else if (key === 'policy.command') {
480
+ const policy = { ...(config.get('policy') ?? {}) };
481
+ if (value === 'off' || value === 'none') {
482
+ delete policy.command;
483
+ config.set('policy', policy);
484
+ ctx.addMessage('system', '✓ policy.command cleared');
485
+ }
486
+ else {
487
+ config.set('policy', { ...policy, command: value });
488
+ ctx.addMessage('system', '✓ policy.command set');
489
+ }
490
+ }
491
+ else {
492
+ ctx.addMessage('error', `Unknown config key: ${key}`);
899
493
  }
900
- config.set('persona', value);
901
- ctx.setPersona(value);
902
- ctx.addMessage('system', `\u2713 persona set to ${value}`);
903
- }
904
- else if (key === 'fancyOutput') {
905
- const bool = value === 'true';
906
- config.set('fancyOutput', bool);
907
- ctx.addMessage('system', `\u2713 fancyOutput set to ${bool}`);
908
494
  }
909
- else {
910
- ctx.addMessage('error', `Unknown config key: ${key}`);
495
+ catch (err) {
496
+ ctx.addMessage('error', `Failed to set ${key}: ${err instanceof Error ? err.message : String(err)}`);
911
497
  }
912
498
  }
913
- catch (err) {
914
- ctx.addMessage('error', `Failed to set ${key}: ${err instanceof Error ? err.message : String(err)}`);
499
+ else {
500
+ ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}\nsessionLogLimit: ${formatSessionLogLimit(config.get('sessionLogLimit'))} (set > 0 to cap)\n\nChange a setting with /config set <key> <value>`);
915
501
  }
916
502
  break;
917
503
  }
918
504
  case '/setup':
919
505
  ctx.addMessage('system', 'Run `calliope --setup` to reconfigure.');
920
506
  break;
921
- case '/layout': {
922
- // /layout [classic|response-top|response-bottom|split|zen|focus|dashboard|minimal]
923
- const layoutArg = parts[1];
924
- if (!layoutArg) {
925
- ctx.addMessage('system', `Current layout: ${ctx.layout}
926
-
927
- Available layouts:
928
- classic - Everything in chronological order
929
- response-top - Calliope response at top, tools below
930
- response-bottom - Tools at top, response at bottom (default)
931
- split - Side by side: tools left, response right
932
- zen - Response only, tools hidden — distraction-free
933
- focus - Latest response pinned top, compact tool log
934
- dashboard - Three-panel: stats, response, tools
935
- minimal - No decorations, raw text output
936
-
937
- Usage: /layout <name>`);
938
- break;
939
- }
940
- const validLayouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
941
- if (!validLayouts.includes(layoutArg)) {
942
- ctx.addMessage('error', `Invalid layout. Choose: ${validLayouts.join(', ')}`);
943
- break;
944
- }
945
- config.set('layout', layoutArg);
946
- ctx.setLayout(layoutArg);
947
- ctx.addMessage('system', `\u2713 Layout set to: ${layoutArg}`);
948
- break;
949
- }
950
- case '/density': {
951
- // /density [normal|compact]
952
- const densityArg = parts[1];
953
- if (!densityArg) {
954
- ctx.addMessage('system', `Current density: ${ctx.density}
955
-
956
- Available densities:
957
- normal - Standard spacing
958
- compact - Reduced whitespace for more info
959
-
960
- Usage: /density <normal|compact>`);
961
- break;
962
- }
963
- const validDensities = ['normal', 'compact'];
964
- if (!validDensities.includes(densityArg)) {
965
- ctx.addMessage('error', `Invalid density. Choose: normal, compact`);
966
- break;
967
- }
968
- config.set('density', densityArg);
969
- ctx.setDensity(densityArg);
970
- ctx.addMessage('system', `\u2713 Density set to: ${densityArg}`);
971
- break;
972
- }
973
- case '/collapse': {
974
- // /collapse [tools|thinking|all|off] [limit N]
975
- const subCmd = parts[1];
976
- if (!subCmd) {
977
- ctx.addMessage('system', `Collapse settings:
978
- collapseTools: ${ctx.collapseSettings.collapseTools}
979
- collapseThinking: ${ctx.collapseSettings.collapseThinking}
980
- toolDisplayLimit: ${ctx.collapseSettings.toolDisplayLimit} (0 = all expanded)
981
-
982
- Usage:
983
- /collapse tools - Toggle tool output collapsing
984
- /collapse thinking - Toggle thinking block collapsing
985
- /collapse all - Collapse both tools and thinking
986
- /collapse off - Expand everything
987
- /collapse limit <N> - Show last N tools expanded (0 = all)`);
988
- break;
989
- }
990
- if (subCmd === 'tools') {
991
- const newVal = !ctx.collapseSettings.collapseTools;
992
- config.set('collapseTools', newVal);
993
- ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: newVal }));
994
- ctx.addMessage('system', `\u2713 collapseTools set to ${newVal}`);
995
- }
996
- else if (subCmd === 'thinking') {
997
- const newVal = !ctx.collapseSettings.collapseThinking;
998
- config.set('collapseThinking', newVal);
999
- ctx.setCollapseSettings(prev => ({ ...prev, collapseThinking: newVal }));
1000
- ctx.addMessage('system', `\u2713 collapseThinking set to ${newVal}`);
1001
- }
1002
- else if (subCmd === 'all') {
1003
- config.set('collapseTools', true);
1004
- config.set('collapseThinking', true);
1005
- ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: true, collapseThinking: true }));
1006
- ctx.addMessage('system', '\u2713 Collapsing tools and thinking');
1007
- }
1008
- else if (subCmd === 'off') {
1009
- config.set('collapseTools', false);
1010
- config.set('collapseThinking', false);
1011
- ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: false, collapseThinking: false }));
1012
- ctx.addMessage('system', '\u2713 Expanding all output');
1013
- }
1014
- else if (subCmd === 'limit') {
1015
- const limit = parseInt(parts[2], 10);
1016
- if (isNaN(limit) || limit < 0 || limit > 100) {
1017
- ctx.addMessage('error', 'Limit must be 0-100');
1018
- break;
507
+ case '/loop': {
508
+ // /loop stop absorbs the old /cancel-loop, /breakloop, /stop
509
+ if (parts[1] === 'stop') {
510
+ if (ctx.loopActive) {
511
+ ctx.loopCancelledRef.current = true;
512
+ ctx.setLoopActive(false);
513
+ ctx.addMessage('system', '\u{1F6D1} Loop cancelled');
1019
514
  }
1020
- config.set('toolDisplayLimit', limit);
1021
- ctx.setCollapseSettings(prev => ({ ...prev, toolDisplayLimit: limit }));
1022
- ctx.addMessage('system', `\u2713 toolDisplayLimit set to ${limit}`);
1023
- }
1024
- else {
1025
- ctx.addMessage('error', 'Unknown collapse option. Use: tools, thinking, all, off, or limit <N>');
515
+ else {
516
+ ctx.addMessage('system', 'No active loop to cancel');
517
+ }
518
+ break;
1026
519
  }
1027
- break;
1028
- }
1029
- case '/loop': {
1030
520
  // Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1031
521
  if (ctx.loopActive) {
1032
- ctx.addMessage('system', 'Loop already running. Use /breakloop to stop it first.');
522
+ ctx.addMessage('system', 'Loop already running. Use /loop stop to stop it first.');
1033
523
  break;
1034
524
  }
1035
525
  const loopArgs = parts.slice(1).join(' ');
@@ -1045,7 +535,8 @@ Usage:
1045
535
  prompt = quotedMatch[1];
1046
536
  if (!prompt) {
1047
537
  ctx.addMessage('system', `Usage: /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1048
- Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"`);
538
+ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"
539
+ Stop a running loop with /loop stop`);
1049
540
  break;
1050
541
  }
1051
542
  const defaultMaxIterations = resolveIterationLimit(config.get('maxIterations'));
@@ -1063,86 +554,11 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1063
554
  Prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
1064
555
  Max iterations: ${formatIterationLimit(loopMaxIterations)}
1065
556
  ${completionMatch ? `Completion promise: "${completionMatch[1]}"` : isFiniteIterationLimit(loopMaxIterations) ? 'No completion promise (runs until max iterations)' : 'No completion promise (runs until stopped)'}
1066
- Use /breakloop to stop`);
557
+ Use /loop stop to stop`);
1067
558
  // Start the loop execution (non-blocking)
1068
559
  ctx.runLoop(prompt, loopMaxIterations, completionMatch?.[1]);
1069
560
  break;
1070
561
  }
1071
- case '/cancel-loop':
1072
- case '/breakloop':
1073
- case '/stop':
1074
- if (ctx.loopActive) {
1075
- ctx.loopCancelledRef.current = true;
1076
- ctx.setLoopActive(false);
1077
- ctx.addMessage('system', '\u{1F6D1} Loop cancelled');
1078
- }
1079
- else {
1080
- ctx.addMessage('system', 'No active loop to cancel');
1081
- }
1082
- break;
1083
- case '/confirm':
1084
- if (parts[1] === 'on') {
1085
- ctx.setConfirmMode(true);
1086
- ctx.addMessage('system', '\u2713 Confirmation mode ON - will ask before risky operations');
1087
- }
1088
- else if (parts[1] === 'off') {
1089
- ctx.setConfirmMode(false);
1090
- ctx.addMessage('system', '\u26A0\uFE0F Confirmation mode OFF - risky operations will auto-execute');
1091
- }
1092
- else {
1093
- ctx.addMessage('system', `Confirm mode: ${ctx.confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
1094
- }
1095
- break;
1096
- case '/profile': {
1097
- const subCmd = parts[1];
1098
- if (subCmd === 'list' || !subCmd) {
1099
- const profiles = config.listProfiles();
1100
- const active = config.getActiveProfile();
1101
- const list = profiles.map(p => {
1102
- const marker = p.name === active ? '\u2192 ' : ' ';
1103
- const tag = p.builtin ? '(built-in)' : '(custom)';
1104
- return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
1105
- }).join('\n');
1106
- ctx.addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
1107
- }
1108
- else if (subCmd === 'save' && parts[2]) {
1109
- const name = parts[2];
1110
- config.saveProfile(name, {
1111
- provider: ctx.provider,
1112
- model: ctx.model,
1113
- persona: ctx.persona,
1114
- confirmMode: ctx.confirmMode,
1115
- });
1116
- ctx.addMessage('system', `\u2713 Saved profile: ${name}`);
1117
- }
1118
- else if (subCmd === 'delete' && parts[2]) {
1119
- const name = parts[2];
1120
- if (config.deleteProfile(name)) {
1121
- ctx.addMessage('system', `\u2713 Deleted profile: ${name}`);
1122
- }
1123
- else {
1124
- ctx.addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
1125
- }
1126
- }
1127
- else {
1128
- // Load profile
1129
- const profile = config.getProfile(subCmd);
1130
- if (profile) {
1131
- ctx.setProvider(profile.provider);
1132
- if (profile.model)
1133
- ctx.setModel(profile.model);
1134
- ctx.setPersona(profile.persona);
1135
- if (profile.confirmMode !== undefined)
1136
- ctx.setConfirmMode(profile.confirmMode);
1137
- config.setActiveProfile(subCmd);
1138
- ctx.addMessage('system', `\u2713 Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
1139
- }
1140
- else {
1141
- ctx.addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
1142
- }
1143
- }
1144
- break;
1145
- }
1146
562
  case '/mcp': {
1147
563
  const subCmd = parts[1];
1148
564
  if (subCmd === 'list' || !subCmd) {
@@ -1152,7 +568,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1152
568
  }
1153
569
  else {
1154
570
  const list = servers.map(s => {
1155
- const status = s.status === 'connected' ? '\u{1F7E2}' : s.status === 'error' ? '\u{1F534}' : '\u26AA';
571
+ const status = s.status === 'connected' ? '\u{1F7E2}' : s.status === 'error' ? '\u{1F534}' : '⚪';
1156
572
  return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
1157
573
  }).join('\n\n');
1158
574
  ctx.addMessage('system', `MCP Servers:\n\n${list}`);
@@ -1163,7 +579,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1163
579
  ctx.addMessage('system', `Registering MCP server: ${url}...`);
1164
580
  try {
1165
581
  const server = await mcp.registerServer(url);
1166
- ctx.addMessage('system', `\u2713 Registered: ${server.name} (${server.tools.length} tools)`);
582
+ ctx.addMessage('system', `✓ Registered: ${server.name} (${server.tools.length} tools)`);
1167
583
  }
1168
584
  catch (e) {
1169
585
  ctx.addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
@@ -1171,7 +587,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1171
587
  }
1172
588
  else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
1173
589
  if (mcp.unregisterServer(parts[2])) {
1174
- ctx.addMessage('system', '\u2713 Server removed');
590
+ ctx.addMessage('system', '✓ Server removed');
1175
591
  }
1176
592
  else {
1177
593
  ctx.addMessage('error', 'Server not found');
@@ -1193,7 +609,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1193
609
  ctx.addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
1194
610
  }
1195
611
  else {
1196
- const list = tools.map(t => `\u2022 ${t.name}\n ${t.description}`).join('\n\n');
612
+ const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
1197
613
  ctx.addMessage('system', `MCP Tools:\n\n${list}`);
1198
614
  }
1199
615
  }
@@ -1205,14 +621,20 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1205
621
  case '/skills': {
1206
622
  const subCmd = parts[1];
1207
623
  if (subCmd === 'list' || !subCmd) {
1208
- const allSkills = skills.getSkills();
1209
- if (allSkills.length === 0) {
624
+ // Show every installed skill with its trust state (#137) — including
625
+ // CHANGED entries that getSkills() withholds from the model.
626
+ const listed = skills.listSkills();
627
+ if (listed.length === 0) {
1210
628
  ctx.addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
1211
629
  }
1212
630
  else {
1213
- const list = allSkills.map(s => {
631
+ const list = listed.map(s => {
1214
632
  const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
1215
- return `\u2022 ${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
633
+ const trust = s.trust === 'pinned' ? (s.fingerprint ?? 'pinned') :
634
+ s.trust === 'changed' ? 'CHANGED — reinstall to re-trust' :
635
+ 'UNVERIFIED';
636
+ const desc = s.description ? `${s.description.substring(0, 80)}...` : '';
637
+ return `• ${s.name} ${src} [${trust}]\n ${desc}`;
1216
638
  }).join('\n\n');
1217
639
  ctx.addMessage('system', `Installed Skills:\n\n${list}`);
1218
640
  }
@@ -1220,6 +642,10 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1220
642
  else if (subCmd === 'add' && parts[2]) {
1221
643
  const source = parts[2];
1222
644
  ctx.addMessage('system', `Installing skill: ${source}...`);
645
+ // Snapshot pinned fingerprints so a re-install can show old→new rather
646
+ // than re-pinning silently (#137). The remote name is only known after
647
+ // the fetch, so we key the lookup by the returned skill's name.
648
+ const priorFingerprints = new Map(skills.listSkills().map(s => [s.name, s.fingerprint]));
1223
649
  try {
1224
650
  let skill;
1225
651
  if (source.startsWith('http')) {
@@ -1232,7 +658,14 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1232
658
  skill = await skills.installFromRegistry(source);
1233
659
  }
1234
660
  if (skill) {
1235
- ctx.addMessage('system', `\u2713 Installed: ${skill.metadata.name}`);
661
+ const prev = priorFingerprints.get(skill.metadata.name);
662
+ const now = skill.fingerprint ?? '(unverified)';
663
+ if (prev && prev !== skill.fingerprint) {
664
+ ctx.addMessage('system', `✓ Re-pinned ${skill.metadata.name}: ${prev} → ${now} (content updated)`);
665
+ }
666
+ else {
667
+ ctx.addMessage('system', `✓ Installed ${skill.metadata.name} — pinned ${now} (trust-on-first-use)`);
668
+ }
1236
669
  }
1237
670
  else {
1238
671
  ctx.addMessage('error', 'Failed to install skill');
@@ -1244,7 +677,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1244
677
  }
1245
678
  else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
1246
679
  if (skills.uninstallSkill(parts[2])) {
1247
- ctx.addMessage('system', '\u2713 Skill removed');
680
+ ctx.addMessage('system', '✓ Skill removed');
1248
681
  }
1249
682
  else {
1250
683
  ctx.addMessage('error', 'Skill not found');
@@ -1261,10 +694,19 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1261
694
  info += `License: ${skill.metadata.license}\n`;
1262
695
  if (skill.sourceUrl)
1263
696
  info += `Source: ${skill.sourceUrl}\n`;
697
+ info += skill.fingerprint ? `Fingerprint: ${skill.fingerprint} (${skill.trust})\n` : 'Fingerprint: none (UNVERIFIED)\n';
1264
698
  ctx.addMessage('system', info);
1265
699
  }
1266
700
  else {
1267
- ctx.addMessage('error', 'Skill not found');
701
+ // getSkill withholds a tampered skill; surface WHY, not a bare
702
+ // "not found", so the trust state stays visible (#137).
703
+ const changed = skills.listSkills().find(s => s.name === parts[2] && s.trust === 'changed');
704
+ if (changed) {
705
+ ctx.addMessage('error', `Skill "${parts[2]}" CHANGED since install (pinned ${changed.fingerprint}) — content no longer matches. Reinstall to re-trust.`);
706
+ }
707
+ else {
708
+ ctx.addMessage('error', 'Skill not found');
709
+ }
1268
710
  }
1269
711
  }
1270
712
  else {
@@ -1341,894 +783,55 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1341
783
  }
1342
784
  break;
1343
785
  }
1344
- case '/find': {
1345
- const fuzzy = await import('../fuzzy-search.js');
1346
- const query = parts.slice(1).join(' ');
1347
- if (!query) {
1348
- ctx.addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
1349
- }
1350
- else {
1351
- const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
1352
- if (results.length === 0) {
1353
- ctx.addMessage('system', 'No files found');
1354
- }
1355
- else {
1356
- const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
1357
- ctx.addMessage('system', `Found ${results.length} files:\n\n${list}`);
1358
- }
1359
- }
1360
- break;
1361
- }
1362
- case '/branch': {
1363
- const branching = await import('../branching.js');
786
+ case '/scope': {
1364
787
  const subCmd = parts[1];
1365
- const sessionId = ctx.sessionRef.current?.id || storage.createSessionId();
1366
- if (subCmd === 'list' || !subCmd) {
1367
- const tree = branching.getBranchTree(sessionId);
1368
- ctx.addMessage('system', `Branches:\n${tree}`);
1369
- }
1370
- else if (subCmd === 'new' && parts[2]) {
1371
- const branch = branching.createBranch(sessionId, parts[2], ctx.llmMessages.current, parts.slice(3).join(' '));
1372
- ctx.addMessage('system', `Created branch: ${branch.name}`);
1373
- }
1374
- else if (subCmd === 'switch' && parts[2]) {
1375
- const msgs = branching.switchBranch(sessionId, parts[2], ctx.llmMessages.current);
1376
- if (msgs) {
1377
- ctx.llmMessages.current = msgs;
1378
- ctx.addMessage('system', `Switched to branch: ${parts[2]}`);
788
+ if (subCmd === 'add') {
789
+ // Absorbs the old /add-dir
790
+ const dirPath = parts.slice(2).join(' ').replace(/^["']|["']$/g, '');
791
+ if (!dirPath) {
792
+ ctx.addMessage('system', 'Usage: /scope add <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
1379
793
  }
1380
794
  else {
1381
- ctx.addMessage('error', 'Branch not found');
795
+ const result = addToScope(dirPath);
796
+ if (result.success) {
797
+ ctx.addMessage('system', `✓ ${result.message}`);
798
+ }
799
+ else {
800
+ ctx.addMessage('error', result.message);
801
+ }
1382
802
  }
1383
803
  }
1384
- else if (subCmd === 'delete' && parts[2]) {
1385
- if (branching.deleteBranch(sessionId, parts[2])) {
1386
- ctx.addMessage('system', 'Branch deleted');
804
+ else if (subCmd === 'remove') {
805
+ // Absorbs the old /remove-dir
806
+ const dirPath = parts.slice(2).join(' ').replace(/^["']|["']$/g, '');
807
+ if (!dirPath) {
808
+ ctx.addMessage('system', 'Usage: /scope remove <path>\n\nRemove a directory from the allowed scope.');
1387
809
  }
1388
810
  else {
1389
- ctx.addMessage('error', 'Cannot delete branch');
1390
- }
1391
- }
1392
- else {
1393
- ctx.addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
1394
- }
1395
- break;
1396
- }
1397
- case '/theme': {
1398
- const subCmd = parts[1];
1399
- if (!subCmd) {
1400
- // Open interactive theme picker
1401
- ctx.setModalMode('theme-picker');
1402
- }
1403
- else if (subCmd === 'list') {
1404
- const themes = await import('../themes.js');
1405
- const list = themes.listThemes();
1406
- const current = themes.getCurrentThemeName();
1407
- const formatted = list.map((t) => {
1408
- const marker = t.name === current ? ' *' : '';
1409
- const custom = t.custom ? ' (custom)' : '';
1410
- return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
1411
- }).join('\n');
1412
- ctx.addMessage('system', `Available themes:\n${formatted}`);
1413
- }
1414
- else {
1415
- const themes = await import('../themes.js');
1416
- if (themes.setCurrentTheme(subCmd)) {
1417
- themes.clearThemeCache();
1418
- ctx.addMessage('system', `Theme set to: ${subCmd}`);
1419
- }
1420
- else {
1421
- ctx.addMessage('error', `Theme not found: ${subCmd}`);
1422
- }
1423
- }
1424
- break;
1425
- }
1426
- case '/skin': {
1427
- const subCmd = parts[1];
1428
- if (subCmd === 'list' || !subCmd) {
1429
- const skins = listSkins();
1430
- const current = getCurrentSkin();
1431
- const formatted = skins.map((s) => {
1432
- const marker = s.name === current.name ? ' *' : '';
1433
- const custom = s.custom ? ' (custom)' : '';
1434
- return ` ${s.name}${marker}${custom} - ${s.description}`;
1435
- }).join('\n');
1436
- ctx.addMessage('system', `Active: ${current.name}\nAvailable skins:\n${formatted}`);
1437
- }
1438
- else {
1439
- applySkin(subCmd);
1440
- const newSkin = getCurrentSkin();
1441
- if (newSkin.name === subCmd) {
1442
- config.set('activeSkin', subCmd);
1443
- ctx.addMessage('system', `Skin set to: ${subCmd} \u2014 ${newSkin.description}`);
1444
- }
1445
- else {
1446
- ctx.addMessage('error', `Skin not found: ${subCmd}. Available: ${listSkins().map((s) => s.name).join(', ')}`);
1447
- }
1448
- }
1449
- break;
1450
- }
1451
- case '/palette': {
1452
- const subCmd = parts[1];
1453
- if (subCmd === 'list' || !subCmd) {
1454
- const palettes = listPalettes();
1455
- const current = getCurrentPalette();
1456
- const formatted = palettes.map((p) => {
1457
- const marker = p.name === current.name ? ' *' : '';
1458
- const custom = p.custom ? ' (custom)' : '';
1459
- return ` ${p.name}${marker}${custom} - ${p.description}`;
1460
- }).join('\n');
1461
- ctx.addMessage('system', `Active: ${current.name}\nAvailable palettes:\n${formatted}`);
1462
- }
1463
- else {
1464
- applyPalette(subCmd);
1465
- const newPal = getCurrentPalette();
1466
- if (newPal.name === subCmd) {
1467
- config.set('activePalette', subCmd);
1468
- ctx.addMessage('system', `Palette set to: ${subCmd} \u2014 ${newPal.description}`);
1469
- }
1470
- else {
1471
- ctx.addMessage('error', `Palette not found: ${subCmd}. Available: ${listPalettes().map((p) => p.name).join(', ')}`);
1472
- }
1473
- }
1474
- break;
1475
- }
1476
- case '/companion': {
1477
- const subCmd = parts[1];
1478
- if (subCmd === 'list' || !subCmd) {
1479
- const companions = listCompanions();
1480
- const current = getCurrentCompanion();
1481
- const formatted = companions.map((comp) => {
1482
- const marker = comp.name === current.name ? ' *' : '';
1483
- return ` ${comp.name}${marker} - ${comp.description}`;
1484
- }).join('\n');
1485
- ctx.addMessage('system', `Active: ${current.name}\nAvailable companions:\n${formatted}`);
1486
- }
1487
- else {
1488
- applyCompanion(subCmd);
1489
- const newComp = getCurrentCompanion();
1490
- if (newComp.name === subCmd) {
1491
- config.set('activeCompanion', subCmd);
1492
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1493
- ctx.addMessage('system', `Companion set to: ${subCmd} \u2014 "${newComp.greeting}"`);
1494
- }
1495
- else {
1496
- ctx.addMessage('error', `Companion not found: ${subCmd}. Available: ${listCompanions().map((c) => c.name).join(', ')}`);
1497
- }
1498
- }
1499
- break;
1500
- }
1501
- case '/banner': {
1502
- const bannerSkin = getCurrentSkin();
1503
- const bannerPalette = getCurrentPalette();
1504
- const bannerColor = bannerPalette.colors.primary;
1505
- const imgInfo = getTerminalImageInfo();
1506
- const rendered = renderSkinBanner(bannerSkin.banner.art, bannerColor, bannerSkin.banner.tagline ?? undefined, imgInfo.mode);
1507
- ctx.addMessage('system', `${rendered}\n\nSkin: ${bannerSkin.name} | Terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''}`);
1508
- break;
1509
- }
1510
- case '/hud': {
1511
- const hudSkin = getCurrentSkin();
1512
- const hudPalette = getCurrentPalette();
1513
- const hudCompanion = getCurrentCompanion();
1514
- const hudPack = getCurrentPack();
1515
- const hudIntensity = getCompanionMode();
1516
- ctx.addMessage('system', `HUD Configuration\n` +
1517
- (hudPack ? ` Pack: ${hudPack.name} — ${hudPack.description}\n` : '') +
1518
- ` Skin: ${hudSkin.name} — ${hudSkin.description}\n` +
1519
- ` Palette: ${hudPalette.name} — ${hudPalette.description}\n` +
1520
- ` Companion: ${hudCompanion.name} — ${hudCompanion.description}\n` +
1521
- ` Intensity: ${hudIntensity}\n` +
1522
- ` Emojis: ${config.get('useEmojis') !== false ? 'ON' : 'OFF'}\n` +
1523
- ` Mood: ${getMoodText()}\n\n` +
1524
- ` /pack <name> /intensity <pro|immersive> /emoji [on|off]\n` +
1525
- ` /skin <name> /palette <name> /companion <name>`);
1526
- break;
1527
- }
1528
- case '/pack': {
1529
- const subCmd = parts[1];
1530
- if (!subCmd) {
1531
- ctx.setModalMode('pack-picker');
1532
- break;
1533
- }
1534
- if (subCmd === 'list') {
1535
- const category = parts[2];
1536
- const packs = listThemePacks(category || undefined);
1537
- const currentP = getCurrentPack();
1538
- // Group by category
1539
- const grouped = new Map();
1540
- for (const p of packs) {
1541
- const group = grouped.get(p.category) || [];
1542
- group.push(p);
1543
- grouped.set(p.category, group);
1544
- }
1545
- let output = 'Theme Packs:\n';
1546
- for (const [cat, catPacks] of grouped) {
1547
- output += `\n [${cat}]\n`;
1548
- for (const p of catPacks) {
1549
- const marker = currentP && p.name === currentP.name ? ' *' : '';
1550
- output += ` ${p.name}${marker} — ${p.description}\n`;
1551
- }
1552
- }
1553
- output += '\nUse: /pack <name>';
1554
- ctx.addMessage('system', output);
1555
- }
1556
- else {
1557
- // Run theme transition animation before applying
1558
- const targetPack = getThemePack(subCmd);
1559
- if (targetPack?.skin.splash?.transition) {
1560
- await renderTransition(targetPack.skin.splash.transition);
1561
- }
1562
- const success = applyThemePack(subCmd, getCompanionMode());
1563
- if (success) {
1564
- const pack = getCurrentPack();
1565
- config.set('activeThemePack', subCmd);
1566
- config.set('activeSkin', pack.skin.name);
1567
- config.set('activePalette', pack.palette.name);
1568
- const companion = getCompanionMode() === 'professional'
1569
- ? pack.companions.professional
1570
- : pack.companions.immersive;
1571
- config.set('activeCompanion', companion.name);
1572
- // Reset LLM system prompt to use the companion's persona
1573
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1574
- ctx.addMessage('system', `Theme pack: ${subCmd}\n` +
1575
- ` Skin: ${pack.skin.name}, Palette: ${pack.palette.name}, Companion: ${companion.name}\n` +
1576
- ` "${companion.greeting}"`);
1577
- }
1578
- else {
1579
- ctx.addMessage('error', `Theme pack not found: ${subCmd}. Use /pack list to see available packs.`);
1580
- }
1581
- }
1582
- break;
1583
- }
1584
- case '/intensity': {
1585
- const intensity = parts[1];
1586
- if (intensity === 'professional' || intensity === 'pro') {
1587
- const success = setCompanionMode('professional');
1588
- if (success) {
1589
- const pack = getCurrentPack();
1590
- config.set('companionIntensity', 'professional');
1591
- config.set('activeCompanion', pack.companions.professional.name);
1592
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1593
- ctx.addMessage('system', `Switched to professional mode — ${pack.companions.professional.description}`);
1594
- }
1595
- else {
1596
- ctx.addMessage('error', 'No theme pack active. Use /pack <name> first.');
1597
- }
1598
- }
1599
- else if (intensity === 'immersive' || intensity === 'imm') {
1600
- const success = setCompanionMode('immersive');
1601
- if (success) {
1602
- const pack = getCurrentPack();
1603
- config.set('companionIntensity', 'immersive');
1604
- config.set('activeCompanion', pack.companions.immersive.name);
1605
- ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1606
- ctx.addMessage('system', `Switched to immersive mode — ${pack.companions.immersive.description}`);
1607
- }
1608
- else {
1609
- ctx.addMessage('error', 'No theme pack active. Use /pack <name> first.');
1610
- }
1611
- }
1612
- else {
1613
- const currentIntensity = getCompanionMode();
1614
- ctx.addMessage('system', `Intensity: ${currentIntensity}\nOptions: /intensity professional (pro), /intensity immersive (imm)`);
1615
- }
1616
- break;
1617
- }
1618
- case '/emoji': {
1619
- const emojiArg = parts[1];
1620
- const current = config.get('useEmojis') !== false;
1621
- if (emojiArg === 'on') {
1622
- config.set('useEmojis', true);
1623
- ctx.addMessage('system', '\u2713 Emojis enabled');
1624
- }
1625
- else if (emojiArg === 'off') {
1626
- config.set('useEmojis', false);
1627
- ctx.addMessage('system', '\u2713 Emojis disabled — text fallbacks will be used');
1628
- }
1629
- else if (emojiArg === 'toggle') {
1630
- config.set('useEmojis', !current);
1631
- ctx.addMessage('system', `\u2713 Emojis ${!current ? 'enabled' : 'disabled'}`);
1632
- }
1633
- else {
1634
- ctx.addMessage('system', `Emojis: ${current ? 'ON' : 'OFF'}\nUsage: /emoji [on|off|toggle]`);
1635
- }
1636
- break;
1637
- }
1638
- case '/hooks': {
1639
- const hooksModule = await import('../hooks.js');
1640
- const subCmd = parts[1];
1641
- if (subCmd === 'list' || !subCmd) {
1642
- ctx.addMessage('system', hooksModule.listHooksFormatted());
1643
- }
1644
- else if (subCmd === 'add' && parts[2]) {
1645
- const event = parts[2];
1646
- const hookCommand = parts.slice(3).join(' ');
1647
- if (!hookCommand) {
1648
- ctx.addMessage('system', 'Usage: /hooks add <event> <command>');
1649
- }
1650
- else {
1651
- hooksModule.addHook({ event, name: `Hook for ${event}`, command: hookCommand, enabled: true, async: false });
1652
- ctx.addMessage('system', 'Hook added');
1653
- }
1654
- }
1655
- else if (subCmd === 'init') {
1656
- hooksModule.initDefaultHooks();
1657
- ctx.addMessage('system', 'Default hooks initialized');
1658
- }
1659
- else {
1660
- ctx.addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
1661
- }
1662
- break;
1663
- }
1664
- case '/search': {
1665
- const query = parts.slice(1).join(' ');
1666
- if (!query) {
1667
- ctx.addMessage('system', 'Usage: /search <query>\nSearch conversation history');
1668
- }
1669
- else {
1670
- const lower = query.toLowerCase();
1671
- const matches = ctx.messages.filter(m => m.content.toLowerCase().includes(lower));
1672
- if (matches.length === 0) {
1673
- ctx.addMessage('system', 'No matches found');
1674
- }
1675
- else {
1676
- const results = matches.slice(-10).map(m => {
1677
- const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
1678
- return `[${m.type}] ${preview}...`;
1679
- }).join('\n\n');
1680
- ctx.addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
1681
- }
1682
- }
1683
- break;
1684
- }
1685
- case '/project': {
1686
- const projectConfig = await import('../project-config.js');
1687
- const subCmd = parts[1];
1688
- const cwd = process.cwd();
1689
- if (subCmd === 'init') {
1690
- const configPath = projectConfig.createProjectConfig(cwd);
1691
- ctx.addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
1692
- }
1693
- else if (subCmd === 'show' || !subCmd) {
1694
- const configPath = projectConfig.findProjectConfig(cwd);
1695
- if (!configPath) {
1696
- ctx.addMessage('system', 'No project config found.\nRun /project init to create one.');
1697
- }
1698
- else {
1699
- const cfg = projectConfig.loadProjectConfig(configPath);
1700
- if (cfg) {
1701
- let info = `Config: ${configPath}\n\n`;
1702
- if (cfg.project)
1703
- info += `Project: ${cfg.project}\n`;
1704
- if (cfg.provider)
1705
- info += `Provider: ${cfg.provider}\n`;
1706
- if (cfg.model)
1707
- info += `Model: ${cfg.model}\n`;
1708
- if (cfg.tech?.length)
1709
- info += `Tech: ${cfg.tech.join(', ')}\n`;
1710
- if (cfg.conventions?.length)
1711
- info += `\nConventions:\n${cfg.conventions.map((c) => ` - ${c}`).join('\n')}\n`;
1712
- if (cfg.commands)
1713
- info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
1714
- ctx.addMessage('system', info);
811
+ const result = removeFromScope(dirPath);
812
+ if (result.success) {
813
+ ctx.addMessage('system', `✓ ${result.message}`);
1715
814
  }
1716
815
  else {
1717
- ctx.addMessage('error', 'Failed to parse config');
816
+ ctx.addMessage('error', result.message);
1718
817
  }
1719
818
  }
1720
819
  }
1721
- else if (subCmd === 'run' && parts[2]) {
1722
- const configPath = projectConfig.findProjectConfig(cwd);
1723
- const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
1724
- const cmdName = parts[2];
1725
- if (cfg?.commands?.[cmdName]) {
1726
- const commandToRun = cfg.commands[cmdName];
1727
- // Show the command and source for user awareness
1728
- ctx.addMessage('system', `Project command "${cmdName}" from ${configPath}:\n` +
1729
- ` $ ${commandToRun}\n\n` +
1730
- `Type "/project run-confirm ${cmdName}" to execute, or review the .calliope config first.`);
1731
- }
1732
- else {
1733
- ctx.addMessage('error', `Command not found: ${cmdName}`);
1734
- }
1735
- }
1736
- else if (subCmd === 'run-confirm' && parts[2]) {
1737
- const configPath = projectConfig.findProjectConfig(cwd);
1738
- const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
1739
- const cmdName = parts[2];
1740
- if (cfg?.commands?.[cmdName]) {
1741
- const commandToRun = cfg.commands[cmdName];
1742
- ctx.addMessage('system', `Running: ${commandToRun}`);
1743
- const { spawn } = await import('child_process');
1744
- const proc = spawn('sh', ['-c', commandToRun], { cwd, stdio: 'pipe' });
1745
- let output = '';
1746
- proc.stdout?.on('data', (d) => output += d.toString());
1747
- proc.stderr?.on('data', (d) => output += d.toString());
1748
- proc.on('close', (code) => {
1749
- ctx.addMessage('system', `Exit ${code}\n${output}`);
1750
- });
1751
- }
1752
- else {
1753
- ctx.addMessage('error', `Command not found: ${cmdName}`);
1754
- }
1755
- }
1756
- else {
1757
- ctx.addMessage('system', 'Usage: /project [init|show|run <cmd>|run-confirm <cmd>]');
1758
- }
1759
- break;
1760
- }
1761
- case '/route':
1762
- case '/autoroute': {
1763
- if (parts[1] === 'on') {
1764
- ctx.setAutoRoute(true);
1765
- ctx.addMessage('system', '\u2713 Auto-routing ON - model selected based on task complexity');
1766
- }
1767
- else if (parts[1] === 'off') {
1768
- ctx.setAutoRoute(false);
1769
- ctx.addMessage('system', '\u2713 Auto-routing OFF - using fixed model');
1770
- }
1771
- else if (parts[1] === 'test' && parts[2]) {
1772
- const testMsg = parts.slice(2).join(' ');
1773
- const decision = modelRouter.routeRequest(testMsg, ctx.actualProvider);
1774
- ctx.addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
1775
- }
1776
- else {
1777
- const tiers = modelRouter.getAllTiers(ctx.actualProvider);
1778
- ctx.addMessage('system', `Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${ctx.actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
1779
- }
1780
- break;
1781
- }
1782
- case '/summarize': {
1783
- const subCmd = parts[1];
1784
- if (subCmd === 'context' || !subCmd) {
1785
- const msgCount = ctx.llmMessages.current.length;
1786
- if (msgCount < 5) {
1787
- ctx.addMessage('system', 'Not enough messages to summarize.');
1788
- }
1789
- else {
1790
- const summary = summarization.extractKeyInfo(ctx.llmMessages.current);
1791
- let info = 'Context Summary:\n\n';
1792
- if (summary.topics.length)
1793
- info += `**Topics:** ${summary.topics.join(', ')}\n`;
1794
- if (summary.decisions.length)
1795
- info += `**Decisions:**\n${summary.decisions.map((d) => ` - ${d}`).join('\n')}\n`;
1796
- if (summary.actions.length)
1797
- info += `**Actions:**\n${summary.actions.map((a) => ` - ${a}`).join('\n')}\n`;
1798
- if (summary.codeChanges.length)
1799
- info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map((c) => ` - ${c}`).join('\n')}\n`;
1800
- ctx.addMessage('system', info || 'No key information extracted.');
1801
- }
1802
- }
1803
- else if (subCmd === 'compact') {
1804
- // Summarize and compact the conversation
1805
- const result = summarization.summarizeConversation(ctx.llmMessages.current, { maxTokens: 50000 });
1806
- if (result.summarizedCount > 0) {
1807
- ctx.llmMessages.current = result.messages;
1808
- ctx.setContextTokens(ctx.estimateContextTokens());
1809
- ctx.addMessage('system', `\u2713 Compacted ${result.summarizedCount} messages (${result.originalTokens} \u2192 ${result.reducedTokens} tokens)`);
1810
- }
1811
- else {
1812
- ctx.addMessage('system', 'Context already within limits, no compaction needed.');
1813
- }
1814
- }
1815
- else {
1816
- ctx.addMessage('system', 'Usage: /summarize [context|compact]');
1817
- }
1818
- break;
1819
- }
1820
- case '/upgrade':
1821
- ctx.addMessage('system', 'Checking for updates...');
1822
- try {
1823
- const current = getVersion();
1824
- const latest = await getLatestVersion();
1825
- if (!latest) {
1826
- ctx.addMessage('error', 'Could not check for updates');
1827
- break;
1828
- }
1829
- const [cMaj, cMin, cPat] = current.split('.').map(Number);
1830
- const [lMaj, lMin, lPat] = latest.split('.').map(Number);
1831
- const hasUpdate = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
1832
- if (hasUpdate) {
1833
- ctx.setLatestVersion(latest);
1834
- ctx.setModalMode('upgrade');
1835
- }
1836
- else {
1837
- ctx.addMessage('system', `You're on the latest version (v${current})`);
1838
- }
1839
- }
1840
- catch (e) {
1841
- ctx.addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
1842
- }
1843
- break;
1844
- case '/session':
1845
- case '/sessions':
1846
- if (parts[1] === 'list' || !parts[1]) {
1847
- const sessions = storage.listSessions(20);
1848
- if (sessions.length === 0) {
1849
- ctx.addMessage('system', 'No previous sessions found.');
1850
- }
1851
- else {
1852
- ctx.setAvailableSessions(sessions);
1853
- ctx.setModalMode('sessions');
1854
- }
1855
- }
1856
- else if (parts[1] === 'info') {
1857
- const session = ctx.sessionRef.current;
1858
- if (session) {
1859
- const savedMessages = storage.loadMessageHistory();
1860
- const savedCount = savedMessages ? savedMessages.length : 0;
1861
- const ledgerTotals = ctx.ledger?.getTotals();
1862
- const latestRun = ctx.ledger?.getLatestRun();
1863
- const lines = [
1864
- `Session: ${session.projectName}`,
1865
- `ID: ${session.id}`,
1866
- `Created: ${new Date(session.createdAt).toLocaleString()}`,
1867
- `Messages: ${session.messageCount}`,
1868
- `Saved LLM messages: ${savedCount}`,
1869
- ];
1870
- if (ledgerTotals) {
1871
- lines.push(`Iterations logged: ${ledgerTotals.iterations}`, `Failed approaches: ${ctx.ledger?.getFailedApproachCount() ?? 0}`);
1872
- }
1873
- if (latestRun) {
1874
- lines.push(`Latest run: ${formatLedgerRun(latestRun)}`);
1875
- }
1876
- ctx.addMessage('system', lines.join('\n'));
1877
- }
1878
- else {
1879
- ctx.addMessage('system', 'No active session.');
1880
- }
1881
- }
1882
- else if (parts[1] === 'fork') {
1883
- const session = ctx.sessionRef.current;
1884
- if (!session) {
1885
- ctx.addMessage('error', 'No active session to fork.');
1886
- }
1887
- else {
1888
- // Save current messages before forking
1889
- storage.saveMessageHistory(ctx.llmMessages.current);
1890
- if (ctx.ledger) {
1891
- storage.saveIterationLedger(ctx.ledger);
1892
- }
1893
- const forked = storage.forkSession(session.projectPath);
1894
- if (forked) {
1895
- ctx.sessionRef.current = forked;
1896
- ctx.addMessage('system', `Forked session: ${forked.id}\nMessages carried over: ${ctx.llmMessages.current.length}\n\nYou are now on the forked session. The original session is preserved.`);
1897
- }
1898
- else {
1899
- ctx.addMessage('error', 'Failed to fork session. No saved messages found.');
1900
- }
1901
- }
1902
- }
1903
- else if (parts[1] === 'save') {
1904
- storage.saveMessageHistory(ctx.llmMessages.current);
1905
- if (ctx.ledger) {
1906
- storage.saveIterationLedger(ctx.ledger);
1907
- }
1908
- ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages and current log state to session.`);
1909
- }
1910
- else {
1911
- ctx.addMessage('system', 'Usage: /session [list|info|fork|save] or just /sessions');
1912
- }
1913
- break;
1914
- case '/log': {
1915
- if (!ctx.ledger) {
1916
- ctx.addMessage('system', 'No session log available.');
1917
- break;
1918
- }
1919
- const subCmd = parts[1] || 'summary';
1920
- if (subCmd === 'summary') {
1921
- const totals = ctx.ledger.getTotals();
1922
- const runs = ctx.ledger.getRuns(5);
1923
- const allFailures = ctx.ledger.getFailedApproaches();
1924
- const failures = allFailures.slice(-5);
1925
- const lines = [
1926
- 'Session Log',
1927
- `Iterations: ${totals.iterations}`,
1928
- `Failed approaches: ${ctx.ledger.getFailedApproachCount()}`,
1929
- `Tokens: ${totals.totalTokens}`,
1930
- `Cost: $${totals.totalCost.toFixed(4)}`,
1931
- `Duration: ${formatLedgerDuration(totals.totalDurationMs)}`,
1932
- ];
1933
- if (runs.length > 0) {
1934
- lines.push('', 'Recent runs:');
1935
- for (const run of runs) {
1936
- lines.push(` - ${formatLedgerRun(run)}`);
1937
- }
1938
- }
1939
- if (failures.length > 0) {
1940
- lines.push('', 'Recent failures:');
1941
- for (const failure of failures) {
1942
- lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
1943
- }
1944
- }
1945
- ctx.addMessage('system', lines.join('\n'));
1946
- }
1947
- else if (subCmd === 'tail') {
1948
- const limit = parts[2] ? parseInt(parts[2], 10) : 10;
1949
- if (isNaN(limit) || limit <= 0 || limit > 100) {
1950
- ctx.addMessage('error', 'Usage: /log tail [1-100]');
1951
- break;
1952
- }
1953
- const entries = ctx.ledger.getEntries().slice(-limit);
1954
- if (entries.length === 0) {
1955
- ctx.addMessage('system', 'No logged iterations yet.');
1956
- break;
1957
- }
1958
- const lines = ['Recent iterations:'];
1959
- for (const entry of entries) {
1960
- const actions = entry.actions.length > 0
1961
- ? entry.actions.map(action => `${action.tool}(${action.args})${action.result === 'error' ? ' FAILED' : action.result === 'blocked' ? ' BLOCKED' : ''}`).join(', ')
1962
- : 'no tool actions';
1963
- lines.push(` #${entry.iteration} [${entry.outcome}] ${formatLedgerDuration(entry.durationMs)} — ${actions}`);
1964
- }
1965
- ctx.addMessage('system', lines.join('\n'));
1966
- }
1967
- else if (subCmd === 'failures') {
1968
- const failures = ctx.ledger.getFailedApproaches();
1969
- if (failures.length === 0) {
1970
- ctx.addMessage('system', 'No failed approaches recorded.');
1971
- break;
1972
- }
1973
- const lines = ['Failed approaches:'];
1974
- for (const failure of failures.slice(-10)) {
1975
- lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
1976
- }
1977
- ctx.addMessage('system', lines.join('\n'));
1978
- }
1979
- else if (subCmd === 'reset') {
1980
- ctx.ledger.reset();
1981
- ctx.addMessage('system', 'Session log reset.');
1982
- }
1983
- else {
1984
- ctx.addMessage('system', 'Usage: /log [summary|tail [N]|failures|reset]');
1985
- }
1986
- break;
1987
- }
1988
- case '/todo': {
1989
- const subCommand = parts[1];
1990
- if (subCommand === 'add' && parts.length > 2) {
1991
- const content = parts.slice(2).join(' ');
1992
- const isGlobal = content.includes('--global');
1993
- const isHigh = content.includes('--priority') && content.includes('high');
1994
- const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
1995
- const todo = storage.addTodo(cleanContent, {
1996
- global: isGlobal,
1997
- priority: isHigh ? 'high' : 'normal',
1998
- });
1999
- ctx.addMessage('system', `\u2713 TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
2000
- }
2001
- else if (subCommand === 'done' && parts[2]) {
2002
- const id = parts[2];
2003
- const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
2004
- const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
2005
- if (todo) {
2006
- storage.updateTodo(todo.id, { status: 'completed' });
2007
- ctx.addMessage('system', `\u2713 TODO #${id} marked done`);
2008
- }
2009
- else {
2010
- ctx.addMessage('error', `TODO #${id} not found`);
2011
- }
2012
- }
2013
- else if (subCommand === 'list' || !subCommand) {
2014
- const sessionTodos = storage.getSessionTodos();
2015
- const globalTodos = storage.getGlobalTodos();
2016
- const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
2017
- const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
2018
- if (pending.length === 0 && completed.length === 0) {
2019
- ctx.addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
2020
- }
2021
- else {
2022
- let output = '\u{1F4CB} TODOs:\n';
2023
- if (pending.length > 0) {
2024
- output += pending.map(t => ` ${t.priority === 'high' ? '!' : '\u25A1'} #${t.id.slice(-4)} ${t.content}`).join('\n');
2025
- }
2026
- if (completed.length > 0) {
2027
- output += '\n\nCompleted:\n' + completed.map(t => ` \u2713 #${t.id.slice(-4)} ${t.content}`).join('\n');
2028
- }
2029
- ctx.addMessage('system', output);
2030
- }
2031
- }
2032
- else if (subCommand === 'work' && parts[2]) {
2033
- const id = parts[2];
2034
- const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
2035
- const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
2036
- if (todo) {
2037
- storage.setActiveTodo(todo.id);
2038
- storage.updateTodo(todo.id, { status: 'in_progress' });
2039
- ctx.addMessage('system', `\u2713 Working on: ${todo.content}\n\nTip: I'll help you complete this task. Describe what you need.`);
2040
- }
2041
- else {
2042
- ctx.addMessage('error', `TODO #${id} not found`);
2043
- }
2044
- }
2045
- else if (subCommand === 'clear') {
2046
- storage.setActiveTodo(null);
2047
- ctx.addMessage('system', '\u2713 Active TODO cleared');
2048
- }
2049
- else {
2050
- ctx.addMessage('system', 'Usage: /todo [add <task>|done <id>|work <id>|clear|list]');
2051
- }
2052
- break;
2053
- }
2054
- case '/plans': {
2055
- const subCommand = parts[1];
2056
- if (subCommand === 'list' || !subCommand) {
2057
- const plans = storage.getPlans();
2058
- if (plans.length === 0) {
2059
- ctx.addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
2060
- }
2061
- else {
2062
- const list = plans.slice(0, 5).map((p) => `${p.status === 'completed' ? '\u2713' : '\u25CB'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
2063
- ctx.addMessage('system', `\u{1F4CB} Plans:\n${list}`);
2064
- }
2065
- }
2066
- else if (subCommand === 'view' && parts[2]) {
2067
- const plans = storage.getPlans();
2068
- const plan = plans.find((p) => p.id.endsWith(parts[2]) || p.id === parts[2]);
2069
- if (plan) {
2070
- const phases = plan.phases.map((ph) => ` ${ph.status === 'completed' ? '\u2713' : '\u25CB'} ${ph.name} (${ph.risk} risk)`).join('\n');
2071
- ctx.addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
2072
- }
2073
- else {
2074
- ctx.addMessage('error', `Plan #${parts[2]} not found`);
2075
- }
2076
- }
2077
- else if (subCommand === 'rerun' && parts[2]) {
2078
- const plans = storage.getPlans();
2079
- const plan = plans.find((p) => p.id.endsWith(parts[2]) || p.id === parts[2]);
2080
- if (plan) {
2081
- // Reset plan status and activate
2082
- plan.status = 'in_progress';
2083
- plan.phases.forEach((ph) => ph.status = 'pending');
2084
- storage.savePlan(plan);
2085
- storage.setActivePlan(plan);
2086
- // Generate prompt for re-execution
2087
- const phaseList = plan.phases.map((ph) => `- ${ph.name}`).join('\n');
2088
- const prompt = `Please help me execute this plan:\n\n**${plan.title}**\n\nPhases:\n${phaseList}\n\nStart with the first phase.`;
2089
- ctx.setInput(prompt);
2090
- ctx.addMessage('system', `\u2713 Plan loaded: ${plan.title}\nPress Enter to start execution.`);
2091
- }
2092
- else {
2093
- ctx.addMessage('error', `Plan #${parts[2]} not found`);
2094
- }
2095
- }
2096
- else {
2097
- ctx.addMessage('system', 'Usage: /plans [list|view <id>|rerun <id>]');
2098
- }
2099
- break;
2100
- }
2101
- case '/history': {
2102
- const subCommand = parts[1];
2103
- if (subCommand === 'search' && parts[2]) {
2104
- const query = parts.slice(2).join(' ');
2105
- const results = storage.searchChatHistory(query);
2106
- if (results.length === 0) {
2107
- ctx.addMessage('system', `No matches for "${query}"`);
2108
- }
2109
- else {
2110
- const list = results.slice(-5).map((m) => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
2111
- ctx.addMessage('system', `\u{1F50D} Found ${results.length} matches:\n${list}`);
2112
- }
2113
- }
2114
- else if (subCommand === 'clear') {
2115
- ctx.addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
2116
- }
2117
- else {
2118
- const history = storage.getChatHistory(5);
2119
- if (history.length === 0) {
2120
- ctx.addMessage('system', 'No chat history yet.');
2121
- }
2122
- else {
2123
- const list = history.map((m) => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
2124
- ctx.addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
2125
- }
2126
- }
2127
- break;
2128
- }
2129
- case '/context': {
2130
- const subCommand = parts[1];
2131
- if (subCommand === 'load') {
2132
- const limit = parseInt(parts[2]) || 20;
2133
- const history = storage.getChatHistory(limit);
2134
- if (history.length > 0) {
2135
- // Load history into LLM context
2136
- for (const msg of history) {
2137
- if (msg.role === 'user' || msg.role === 'assistant') {
2138
- ctx.llmMessages.current.push({
2139
- role: msg.role,
2140
- content: msg.content,
2141
- });
2142
- }
2143
- }
2144
- ctx.addMessage('system', `\u2713 Loaded ${history.length} messages into context`);
2145
- }
2146
- else {
2147
- ctx.addMessage('system', 'No history to load.');
2148
- }
2149
- }
2150
- else if (subCommand === 'summary' || !subCommand) {
2151
- // Enhanced context summary with model limits
2152
- const msgCount = ctx.llmMessages.current.length;
2153
- const estTokens = ctx.estimateContextTokens();
2154
- const modelLimit = getModelContextLimit(ctx.actualProvider, ctx.actualModel);
2155
- const percentage = Math.round((estTokens / modelLimit) * 100);
2156
- const formatK = (n) => n >= 1000 ? `${Math.round(n / 1000)}K` : String(n);
2157
- let status = '\u{1F7E2} Healthy';
2158
- if (percentage > 90)
2159
- status = '\u{1F534} Critical';
2160
- else if (percentage > 80)
2161
- status = '\u{1F7E1} Warning';
2162
- else if (percentage > 60)
2163
- status = '\u{1F7E0} Caution';
2164
- ctx.addMessage('system', `**Context Status: ${status}**
2165
-
2166
- **Usage:** ${formatK(estTokens)} / ${formatK(modelLimit)} tokens (${percentage}%)
2167
- **Messages:** ${msgCount}
2168
- **Provider:** ${ctx.actualProvider}
2169
- **Model:** ${ctx.actualModel}
2170
-
2171
- **Commands:**
2172
- /summarize compact - Auto-compress context
2173
- /context load [n] - Load n messages from history
2174
- /clear - Start fresh`);
2175
- }
2176
- else {
2177
- ctx.addMessage('system', 'Usage: /context [load [n]|summary]\n\nShow context status or load history.');
2178
- }
2179
- break;
2180
- }
2181
- case '/scope':
2182
- case '/dirs': {
2183
- const subCmd = parts[1];
2184
- if (subCmd === 'details' || subCmd === 'full') {
820
+ else if (subCmd === 'details' || subCmd === 'full') {
2185
821
  ctx.addMessage('system', getScopeDetails());
2186
822
  }
2187
823
  else if (subCmd === 'reset') {
2188
824
  resetScope(process.cwd());
2189
- ctx.addMessage('system', '\u2713 Scope reset to current directory only');
825
+ ctx.addMessage('system', '✓ Scope reset to current directory only');
2190
826
  }
2191
827
  else {
2192
828
  ctx.addMessage('system', getScopeSummary());
2193
829
  }
2194
830
  break;
2195
831
  }
2196
- case '/add-dir': {
2197
- const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2198
- if (!dirPath) {
2199
- ctx.addMessage('system', 'Usage: /add-dir <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
2200
- }
2201
- else {
2202
- const result = addToScope(dirPath);
2203
- if (result.success) {
2204
- ctx.addMessage('system', `\u2713 ${result.message}`);
2205
- }
2206
- else {
2207
- ctx.addMessage('error', result.message);
2208
- }
2209
- }
2210
- break;
2211
- }
2212
- case '/remove-dir': {
2213
- const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2214
- if (!dirPath) {
2215
- ctx.addMessage('system', 'Usage: /remove-dir <path>\n\nRemove a directory from the allowed scope.');
2216
- }
2217
- else {
2218
- const result = removeFromScope(dirPath);
2219
- if (result.success) {
2220
- ctx.addMessage('system', `\u2713 ${result.message}`);
2221
- }
2222
- else {
2223
- ctx.addMessage('error', result.message);
2224
- }
2225
- }
2226
- break;
2227
- }
2228
- case '/trust':
2229
- case '/untrust': {
832
+ case '/trust': {
2230
833
  const { checkTrust, trustProject, untrustProject, listTrustedProjects, removeFromRegistry } = await import('../trust.js');
2231
- const trustSubCmd = command === '/untrust' ? 'remove' : (parts[1] || 'status');
834
+ const trustSubCmd = parts[1] || 'status';
2232
835
  if (trustSubCmd === 'status') {
2233
836
  const trust = checkTrust(process.cwd());
2234
837
  ctx.addMessage('system', `Trust: ${trust.trusted ? '✓ Trusted' : '✗ Untrusted'}\n${trust.reason}${trust.changed ? '\n⚠️ CALLIOPE.md has changed since trust was granted' : ''}`);
@@ -2238,241 +841,76 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2238
841
  trustProject(dir, parts.slice(3).join(' ') || undefined);
2239
842
  ctx.addMessage('system', `✓ Trusted: ${dir}`);
2240
843
  }
2241
- else if (trustSubCmd === 'remove' || trustSubCmd === 'no') {
2242
- const dir = parts[2] || process.cwd();
2243
- if (command === '/untrust') {
2244
- untrustProject(parts[1] || process.cwd());
2245
- }
2246
- else {
2247
- untrustProject(dir);
2248
- }
2249
- ctx.addMessage('system', `✗ Untrusted: ${command === '/untrust' ? (parts[1] || process.cwd()) : dir}`);
2250
- }
2251
- else if (trustSubCmd === 'list') {
2252
- const projects = listTrustedProjects();
2253
- if (projects.length === 0) {
2254
- ctx.addMessage('system', 'No projects in trust registry.');
2255
- }
2256
- else {
2257
- const list = projects.map(p => ` ${p.entry.trusted ? '✓' : '✗'} ${p.path}${p.entry.note ? ` (${p.entry.note})` : ''}`).join('\n');
2258
- ctx.addMessage('system', `Trust registry:\n${list}`);
2259
- }
2260
- }
2261
- else if (trustSubCmd === 'clear') {
2262
- const dir = parts[2] || process.cwd();
2263
- removeFromRegistry(dir);
2264
- ctx.addMessage('system', `Removed from trust registry: ${dir}`);
2265
- }
2266
- else {
2267
- ctx.addMessage('system', 'Usage: /trust [status|add|remove|list|clear]\n /trust add [path] - trust a project\n /trust remove [path] - untrust a project\n /untrust [path] - shortcut for /trust remove');
2268
- }
2269
- break;
2270
- }
2271
- case '/template':
2272
- case '/t': {
2273
- const subCmd = parts[1];
2274
- if (subCmd === 'list' || !subCmd) {
2275
- if (ctx.templates.length === 0) {
2276
- ctx.addMessage('system', 'No templates saved.\n\nUsage:\n /template save <name> <prompt>\n /template use <name>\n /template delete <name>');
2277
- }
2278
- else {
2279
- const list = ctx.templates.map((t, i) => ` ${i + 1}. ${t.name}: "${t.prompt.substring(0, 50)}${t.prompt.length > 50 ? '...' : ''}"`).join('\n');
2280
- ctx.addMessage('system', `Templates:\n${list}`);
2281
- }
2282
- }
2283
- else if (subCmd === 'save' && parts[2]) {
2284
- const name = parts[2];
2285
- const prompt = parts.slice(3).join(' ').replace(/^["']|["']$/g, '');
2286
- if (!prompt) {
2287
- ctx.addMessage('error', 'Usage: /template save <name> "<prompt>"');
2288
- }
2289
- else {
2290
- storage.saveTemplate(name, prompt);
2291
- ctx.setTemplates(prev => {
2292
- const filtered = prev.filter(t => t.name !== name);
2293
- return [...filtered, { name, prompt, createdAt: new Date() }];
2294
- });
2295
- ctx.addMessage('system', `\u2713 Template saved: ${name}`);
2296
- }
844
+ else if (trustSubCmd === 'remove' || trustSubCmd === 'no') {
845
+ // Absorbs the old /untrust
846
+ const dir = parts[2] || process.cwd();
847
+ untrustProject(dir);
848
+ ctx.addMessage('system', `✗ Untrusted: ${dir}`);
2297
849
  }
2298
- else if (subCmd === 'use' && parts[2]) {
2299
- const name = parts[2];
2300
- const template = ctx.templates.find(t => t.name === name);
2301
- if (template) {
2302
- ctx.setInput(template.prompt);
2303
- ctx.addMessage('system', `\u2713 Template loaded: ${name} (press Enter to send)`);
850
+ else if (trustSubCmd === 'list') {
851
+ const projects = listTrustedProjects();
852
+ if (projects.length === 0) {
853
+ ctx.addMessage('system', 'No projects in trust registry.');
2304
854
  }
2305
855
  else {
2306
- ctx.addMessage('error', `Template not found: ${name}`);
856
+ const list = projects.map(p => ` ${p.entry.trusted ? '✓' : '✗'} ${p.path}${p.entry.note ? ` (${p.entry.note})` : ''}`).join('\n');
857
+ ctx.addMessage('system', `Trust registry:\n${list}`);
2307
858
  }
2308
859
  }
2309
- else if (subCmd === 'delete' && parts[2]) {
2310
- const name = parts[2];
2311
- const found = ctx.templates.find(t => t.name === name);
2312
- if (found) {
2313
- storage.deleteTemplate(name);
2314
- ctx.setTemplates(prev => prev.filter(t => t.name !== name));
2315
- ctx.addMessage('system', `\u2713 Template deleted: ${name}`);
2316
- }
2317
- else {
2318
- ctx.addMessage('error', `Template not found: ${name}`);
2319
- }
860
+ else if (trustSubCmd === 'clear') {
861
+ const dir = parts[2] || process.cwd();
862
+ removeFromRegistry(dir);
863
+ ctx.addMessage('system', `Removed from trust registry: ${dir}`);
2320
864
  }
2321
865
  else {
2322
- ctx.addMessage('system', 'Usage: /template [list|save <name> <prompt>|use <name>|delete <name>]');
866
+ ctx.addMessage('system', 'Usage: /trust [status|add|remove|list|clear]\n /trust add [path] - trust a project\n /trust remove [path] - untrust a project');
2323
867
  }
2324
868
  break;
2325
869
  }
2326
- case '/cost':
2327
- case '/costs': {
870
+ case '/cost': {
2328
871
  const subCmd = parts[1];
2329
872
  if (subCmd === 'reset') {
2330
873
  storage.resetCosts();
2331
- ctx.addMessage('system', '\u2713 Cost tracking reset');
874
+ ctx.addMessage('system', '✓ Cost tracking reset');
2332
875
  }
2333
876
  else {
2334
877
  ctx.addMessage('system', storage.getCostSummary());
2335
878
  }
2336
879
  break;
2337
880
  }
2338
- case '/bookmark':
2339
- case '/bm': {
881
+ case '/compact': {
2340
882
  const subCmd = parts[1];
2341
- if (!subCmd || subCmd === 'list') {
2342
- // List bookmarks
2343
- if (ctx.bookmarks.length === 0) {
2344
- ctx.addMessage('system', 'No bookmarks. Use /bookmark "name" to create one.');
2345
- }
2346
- else {
2347
- const list = ctx.bookmarks.map((b, i) => ` ${i + 1}. \u{1F516} ${b.name} (message #${b.messageIndex})`).join('\n');
2348
- ctx.addMessage('system', `Bookmarks:\n${list}\n\nUse /bookmark goto <number> to jump.`);
2349
- }
2350
- }
2351
- else if (subCmd === 'goto' && parts[2]) {
2352
- const idx = parseInt(parts[2]) - 1;
2353
- if (idx >= 0 && idx < ctx.bookmarks.length) {
2354
- const bm = ctx.bookmarks[idx];
2355
- // Save current state for undo
2356
- ctx.saveUndoState();
2357
- // Restore to bookmark point
2358
- ctx.setMessages(ctx.messages.slice(0, bm.messageIndex + 1));
2359
- ctx.llmMessages.current = ctx.llmMessages.current.slice(0, bm.llmMessageIndex + 1);
2360
- ctx.setContextTokens(ctx.estimateContextTokens());
2361
- ctx.addMessage('system', `\u2713 Jumped to bookmark: ${bm.name}`);
2362
- }
2363
- else {
2364
- ctx.addMessage('error', `Invalid bookmark number. Use /bookmark list to see available.`);
2365
- }
2366
- }
2367
- else if (subCmd === 'delete' && parts[2]) {
2368
- const idx = parseInt(parts[2]) - 1;
2369
- if (idx >= 0 && idx < ctx.bookmarks.length) {
2370
- const removed = ctx.bookmarks[idx];
2371
- ctx.setBookmarks(prev => prev.filter((_, i) => i !== idx));
2372
- ctx.addMessage('system', `\u2713 Deleted bookmark: ${removed.name}`);
883
+ if (subCmd === 'status') {
884
+ const msgCount = ctx.llmMessages.current.length;
885
+ if (msgCount < 5) {
886
+ ctx.addMessage('system', 'Not enough messages to summarize.');
2373
887
  }
2374
888
  else {
2375
- ctx.addMessage('error', 'Invalid bookmark number.');
889
+ const summary = summarization.extractKeyInfo(ctx.llmMessages.current);
890
+ let info = 'Context Summary:\n\n';
891
+ if (summary.topics.length)
892
+ info += `**Topics:** ${summary.topics.join(', ')}\n`;
893
+ if (summary.decisions.length)
894
+ info += `**Decisions:**\n${summary.decisions.map((d) => ` - ${d}`).join('\n')}\n`;
895
+ if (summary.actions.length)
896
+ info += `**Actions:**\n${summary.actions.map((a) => ` - ${a}`).join('\n')}\n`;
897
+ if (summary.codeChanges.length)
898
+ info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map((c) => ` - ${c}`).join('\n')}\n`;
899
+ ctx.addMessage('system', info || 'No key information extracted.');
2376
900
  }
2377
901
  }
2378
902
  else {
2379
- // Create bookmark with given name
2380
- const name = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
2381
- const bm = {
2382
- id: `bm_${Date.now()}`,
2383
- name,
2384
- messageIndex: ctx.messages.length - 1,
2385
- llmMessageIndex: ctx.llmMessages.current.length - 1,
2386
- timestamp: new Date(),
2387
- };
2388
- ctx.setBookmarks(prev => [...prev, bm]);
2389
- ctx.addMessage('system', `\u{1F516} Bookmark created: "${name}"`);
2390
- }
2391
- break;
2392
- }
2393
- case '/queue':
2394
- case '/q': {
2395
- // /q is now queue, use /exit to quit
2396
- if (command === '/q' && !parts[1]) {
2397
- // Just /q with no args shows queue
2398
- if (ctx.queuedMessages.length === 0) {
2399
- ctx.addMessage('system', 'No messages queued. Type while agent is processing to queue feedback.');
2400
- }
2401
- else {
2402
- const list = ctx.queuedMessages.map((m, i) => ` ${i + 1}. ${m.substring(0, 60)}${m.length > 60 ? '...' : ''}`).join('\n');
2403
- ctx.addMessage('system', `\u{1F4E8} Queued messages (${ctx.queuedMessages.length}):\n${list}\n\nUse /queue clear to remove all.`);
2404
- }
2405
- break;
2406
- }
2407
- const subCmd = parts[1];
2408
- if (subCmd === 'clear') {
2409
- const count = ctx.queuedMessages.length;
2410
- ctx.setQueuedMessages([]);
2411
- ctx.addMessage('system', `\u2713 Cleared ${count} queued message${count !== 1 ? 's' : ''}`);
2412
- }
2413
- else if (subCmd === 'show' || !subCmd) {
2414
- if (ctx.queuedMessages.length === 0) {
2415
- ctx.addMessage('system', 'No messages queued.');
2416
- }
2417
- else {
2418
- const list = ctx.queuedMessages.map((m, i) => ` ${i + 1}. ${m}`).join('\n');
2419
- ctx.addMessage('system', `\u{1F4E8} Queued messages:\n${list}`);
2420
- }
2421
- }
2422
- else if (subCmd === 'flush') {
2423
- // Force-process queued messages even if stuck
2424
- if (ctx.queuedMessages.length === 0) {
2425
- ctx.addMessage('system', 'No messages to flush.');
903
+ // Default: summarize and compact the conversation
904
+ const result = summarization.summarizeConversation(ctx.llmMessages.current, { maxTokens: 50000 });
905
+ if (result.summarizedCount > 0) {
906
+ ctx.llmMessages.current = result.messages;
907
+ ctx.setContextTokens(ctx.estimateContextTokens());
908
+ ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
2426
909
  }
2427
910
  else {
2428
- const queued = [...ctx.queuedMessages];
2429
- ctx.setQueuedMessages([]);
2430
- ctx.setIsProcessing(false); // Force reset processing state
2431
- ctx.setThinkingState(null);
2432
- ctx.setStreamingResponse('');
2433
- ctx.addMessage('system', `\u{1F504} Flushing ${queued.length} queued message(s)...`);
2434
- const followUp = queued.length === 1
2435
- ? queued[0]
2436
- : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2437
- setTimeout(() => {
2438
- ctx.setIsProcessing(true);
2439
- ctx.runAgent(followUp).finally(() => {
2440
- ctx.setIsProcessing(false);
2441
- ctx.setThinkingState(null);
2442
- ctx.setStreamingResponse('');
2443
- });
2444
- }, 50);
911
+ ctx.addMessage('system', 'Context already within limits, no compaction needed.');
2445
912
  }
2446
913
  }
2447
- else {
2448
- ctx.addMessage('system', 'Usage: /queue [show|clear|flush]\n\nTip: Type while agent is processing to queue follow-up messages.');
2449
- }
2450
- break;
2451
- }
2452
- case '/flush': {
2453
- // Shortcut for /queue flush - force-process queued messages
2454
- if (ctx.queuedMessages.length === 0) {
2455
- ctx.addMessage('system', 'No messages to flush. Use /debug to see current state.');
2456
- }
2457
- else {
2458
- const queued = [...ctx.queuedMessages];
2459
- ctx.setQueuedMessages([]);
2460
- ctx.setIsProcessing(false); // Force reset processing state
2461
- ctx.setThinkingState(null);
2462
- ctx.setStreamingResponse('');
2463
- ctx.addMessage('system', `\u{1F504} Flushing ${queued.length} queued message(s)...`);
2464
- const followUp = queued.length === 1
2465
- ? queued[0]
2466
- : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2467
- setTimeout(() => {
2468
- ctx.setIsProcessing(true);
2469
- ctx.runAgent(followUp).finally(() => {
2470
- ctx.setIsProcessing(false);
2471
- ctx.setThinkingState(null);
2472
- ctx.setStreamingResponse('');
2473
- });
2474
- }, 50);
2475
- }
2476
914
  break;
2477
915
  }
2478
916
  case '/debug': {
@@ -2503,55 +941,6 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2503
941
  }
2504
942
  break;
2505
943
  }
2506
- case '/unstick': {
2507
- // Emergency reset of processing state
2508
- ctx.setIsProcessing(false);
2509
- ctx.setThinkingState(null);
2510
- ctx.setStreamingResponse('');
2511
- ctx.setLoopActive(false);
2512
- ctx.setModalMode('none');
2513
- ctx.setPendingComplexPrompt(null);
2514
- // Also reset to hybrid mode if stuck in plan mode
2515
- if (ctx.mode === 'plan') {
2516
- ctx.setMode('hybrid');
2517
- ctx.addMessage('system', '\u{1F527} Reset processing state + switched from plan to hybrid mode.');
2518
- }
2519
- else {
2520
- ctx.addMessage('system', '\u{1F527} Reset processing state. You can now submit new messages.');
2521
- }
2522
- break;
2523
- }
2524
- case '/keys':
2525
- case '/?': {
2526
- // Show keybindings modal
2527
- ctx.setModalMode('keys');
2528
- break;
2529
- }
2530
- case '/work': {
2531
- // Quick shortcut to enter work mode
2532
- ctx.setMode('work');
2533
- ctx.addMessage('system', `Mode: ${MODE_CONFIG['work'].icon} ${MODE_CONFIG['work'].label} - ${MODE_CONFIG['work'].description}`);
2534
- break;
2535
- }
2536
- case '/plan': {
2537
- // Quick shortcut to enter plan mode
2538
- ctx.setMode('plan');
2539
- ctx.addMessage('system', `Mode: ${MODE_CONFIG['plan'].icon} ${MODE_CONFIG['plan'].label} - ${MODE_CONFIG['plan'].description}`);
2540
- break;
2541
- }
2542
- case '/approve': {
2543
- // Approve a pending plan and start execution (#19)
2544
- const approveMsg = parts.length > 1
2545
- ? `Plan approved with notes: ${parts.slice(1).join(' ')}. Execute it step by step, updating progress as you complete each step.`
2546
- : 'Plan approved. Execute it step by step, updating progress as you complete each step.';
2547
- // Switch to work mode for execution
2548
- ctx.setMode('work');
2549
- ctx.addMessage('system', `${MODE_CONFIG['work'].icon} Switched to work mode for plan execution`);
2550
- // Send approval as user message to the agent
2551
- ctx.addMessage('user', approveMsg);
2552
- await ctx.runAgent(approveMsg);
2553
- break;
2554
- }
2555
944
  case '/resume': {
2556
945
  // Resume a session by loading saved LLM message history
2557
946
  // Usage: /resume [sessionId] - resume a specific session, or current session if no ID
@@ -2591,7 +980,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2591
980
  ctx.llmMessages.current.length = 0;
2592
981
  ctx.llmMessages.current.push({
2593
982
  role: 'system',
2594
- content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)),
983
+ content: buildFullSystemPrompt(getActiveProjectDir(ctx), ctx.actualProvider),
2595
984
  });
2596
985
  for (const msg of history) {
2597
986
  if (msg.role === 'user' || msg.role === 'assistant') {
@@ -2607,429 +996,22 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2607
996
  }
2608
997
  break;
2609
998
  }
2610
- // ================================================================
2611
- // Circuit Breaker
2612
- // ================================================================
2613
- case '/breaker':
2614
- case '/cb': {
2615
- const subCmd = parts[1];
2616
- if (!ctx.circuitBreaker) {
2617
- ctx.addMessage('system', 'Circuit breakers not initialized. They activate automatically during agent runs.');
2618
- break;
2619
- }
2620
- if (subCmd === 'status' || !subCmd) {
2621
- const statuses = ctx.circuitBreaker.getStatus();
2622
- const stats = ctx.circuitBreaker.getTrackingStats();
2623
- const health = ctx.circuitBreaker.getHealth();
2624
- let msg = `Circuit Breaker Health: ${health.toUpperCase()}\n\n`;
2625
- for (const s of statuses) {
2626
- const icon = s.state === 'open' ? '\u{1f534}' : s.state === 'half-open' ? '\u{1f7e1}' : '\u{1f7e2}';
2627
- msg += `${icon} ${s.type}: ${s.state} (tripped ${s.tripCount}x)`;
2628
- if (s.lastEvent)
2629
- msg += ` - ${s.lastEvent.message}`;
2630
- msg += '\n';
2631
- }
2632
- msg += `\nTracking: ${stats.consecutiveErrors} consecutive errors, ${stats.totalTokens.toLocaleString()} total tokens, $${stats.totalCost.toFixed(2)} cost, ${stats.idleCount} idle`;
2633
- ctx.addMessage('system', msg);
2634
- }
2635
- else if (subCmd === 'resume') {
2636
- const breakerType = parts[2];
2637
- ctx.circuitBreaker.resume(breakerType);
2638
- ctx.setBreakerHealth(ctx.circuitBreaker.getHealth());
2639
- ctx.addMessage('system', `\u2713 Circuit breaker${breakerType ? ` "${breakerType}"` : 's'} resumed (half-open mode, 50% more generous thresholds)`);
2640
- }
2641
- else if (subCmd === 'reset') {
2642
- const breakerType = parts[2];
2643
- ctx.circuitBreaker.reset(breakerType);
2644
- ctx.setBreakerHealth(ctx.circuitBreaker.getHealth());
2645
- ctx.addMessage('system', `\u2713 Circuit breaker${breakerType ? ` "${breakerType}"` : 's'} reset to closed`);
2646
- }
2647
- else if (subCmd === 'off') {
2648
- config.set('circuitBreakersEnabled', false);
2649
- ctx.addMessage('system', '\u2713 Circuit breakers disabled (will take effect on next agent run)');
2650
- }
2651
- else if (subCmd === 'on') {
2652
- config.set('circuitBreakersEnabled', true);
2653
- ctx.addMessage('system', '\u2713 Circuit breakers enabled');
2654
- }
2655
- else {
2656
- ctx.addMessage('system', `Usage: /breaker [status|resume|reset|on|off]
2657
- /breaker resume [type] - Resume tripped breaker (half-open)
2658
- /breaker reset [type] - Reset breaker to closed
2659
- /breaker on|off - Enable/disable circuit breakers
2660
-
2661
- Breaker types: repeated-failure, cost-runaway, infinite-loop, token-burn, stall`);
2662
- }
2663
- break;
2664
- }
2665
- // ================================================================
2666
- // Smart Routing
2667
- // ================================================================
2668
- case '/smart': {
2669
- const subCmd = parts[1];
2670
- if (subCmd === 'on') {
2671
- ctx.setSmartRouteActive(true);
2672
- config.set('smartRoutingEnabled', true);
2673
- ctx.addMessage('system', '\u2713 Smart routing ON - best model selected across all providers');
2674
- }
2675
- else if (subCmd === 'off') {
2676
- ctx.setSmartRouteActive(false);
2677
- config.set('smartRoutingEnabled', false);
2678
- ctx.addMessage('system', '\u2713 Smart routing OFF - using fixed provider/model');
2679
- }
2680
- else if (subCmd === 'cost' && parts[2]) {
2681
- const sensitivity = parseFloat(parts[2]);
2682
- if (isNaN(sensitivity) || sensitivity < 0 || sensitivity > 1) {
2683
- ctx.addMessage('error', 'Cost sensitivity must be between 0 (best quality) and 1 (cheapest)');
2684
- }
2685
- else {
2686
- config.set('smartRoutingCostSensitivity', sensitivity);
2687
- ctx.addMessage('system', `\u2713 Smart routing cost sensitivity set to ${sensitivity} (0=quality, 1=cost)`);
2688
- }
2689
- }
2690
- else if (subCmd === 'test' && parts[2]) {
2691
- const testMsg = parts.slice(2).join(' ');
2692
- const routingConfig = ctx.smartRoutingConfig || {
2693
- ...getDefaultSmartRoutingConfig(),
2694
- enabled: true,
2695
- costSensitivity: config.get('smartRoutingCostSensitivity') ?? 0.3,
2696
- };
2697
- const decision = smartRoute(testMsg, routingConfig);
2698
- const taskInfo = detectTaskType(testMsg);
2699
- let msg = `Smart Route Test:\n`;
2700
- msg += ` Task type: ${taskInfo.taskType} (${Math.round(taskInfo.confidence * 100)}% confidence)\n`;
2701
- msg += ` Complexity: ${decision.complexity}\n`;
2702
- msg += ` Selected: ${decision.selected.provider}/${decision.selected.model} (${decision.selected.tier} tier)\n`;
2703
- msg += ` Score: ${(decision.selected.score * 100).toFixed(1)}\n`;
2704
- if (decision.alternatives.length > 0) {
2705
- msg += ` Alternatives:\n`;
2706
- for (const alt of decision.alternatives.slice(0, 3)) {
2707
- msg += ` - ${alt.provider}/${alt.model} (score: ${(alt.score * 100).toFixed(1)})\n`;
2708
- }
2709
- }
2710
- ctx.addMessage('system', msg);
2711
- }
2712
- else {
2713
- ctx.addMessage('system', `Smart routing: ${ctx.smartRouteActive ? 'ON' : 'OFF'}
2714
- Cost sensitivity: ${config.get('smartRoutingCostSensitivity') ?? 0.3}
2715
-
2716
- Usage: /smart [on|off|cost <0-1>|test <message>]
2717
- /smart on - Enable cross-provider routing
2718
- /smart off - Disable, use fixed provider
2719
- /smart cost <0-1> - Set cost sensitivity (0=quality, 1=cheapest)
2720
- /smart test <msg> - Test routing decision for a message`);
2721
- }
2722
- break;
2723
- }
2724
- // ================================================================
2725
- // Swarm Mode
2726
- // ================================================================
2727
- case '/swarm':
2728
- case '/council': {
2729
- const subCmd = parts[1];
2730
- const swarmCwd = ctx.sessionRef.current?.projectPath ?? process.cwd();
2731
- if (!ctx.agtermEnabled) {
2732
- ctx.addMessage('system', 'Agents mode not enabled. Start with --agents flag to use agent swarms.');
2733
- break;
2734
- }
2735
- // /swarm coordinate — multi-agent coordination (formerly /council)
2736
- if (subCmd === 'coordinate' || subCmd === 'coord') {
2737
- const coordSubCmd = parts[2];
2738
- if (coordSubCmd === 'templates') {
2739
- const templates = Object.values(COUNCIL_TEMPLATES);
2740
- let msg = 'Swarm Coordination Templates:\n';
2741
- for (const t of templates) {
2742
- msg += `\n ${t.name} - ${t.description}`;
2743
- msg += `\n Mode: ${t.mode}, Agents: ${t.members.map(m => m.name).join(', ')}`;
2744
- }
2745
- ctx.addMessage('system', msg);
2746
- }
2747
- else if (coordSubCmd === 'status') {
2748
- const sessionId = parts[3];
2749
- if (sessionId) {
2750
- const session = councilManager.getAllSessions().find(s => s.id.startsWith(sessionId));
2751
- if (session) {
2752
- let msg = councilManager.formatSessionStatus(session);
2753
- if (session.status === 'completed' && session.result) {
2754
- msg += `\n\nResult:\n${session.result.slice(0, 500)}${(session.result.length > 500) ? '...' : ''}`;
2755
- }
2756
- ctx.addMessage('system', msg);
2757
- }
2758
- else {
2759
- ctx.addMessage('system', `Coordination session not found: ${sessionId}`);
2760
- }
2761
- }
2762
- else {
2763
- const sessions = councilManager.getAllSessions();
2764
- if (sessions.length === 0) {
2765
- ctx.addMessage('system', 'No coordination sessions.');
2766
- }
2767
- else {
2768
- const lines = sessions.map(s => ` ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 50)}`);
2769
- ctx.addMessage('system', `Coordination Sessions:\n${lines.join('\n')}`);
2770
- }
2771
- }
2772
- }
2773
- else if (coordSubCmd === 'list') {
2774
- const sessions = councilManager.getAllSessions();
2775
- const stats = councilManager.getStats();
2776
- let msg = `Coordination Stats: ${stats.totalSessions} total, ${stats.activeSessions} active, ${stats.completedSessions} completed, ${stats.failedSessions} failed\n`;
2777
- for (const s of sessions) {
2778
- msg += `\n ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 60)}`;
2779
- }
2780
- ctx.addMessage('system', msg);
2781
- }
2782
- else if (coordSubCmd === 'cancel' && parts[3]) {
2783
- const session = councilManager.getAllSessions().find(s => s.id.startsWith(parts[3]));
2784
- if (session) {
2785
- await councilManager.cancelCouncil(session.id);
2786
- ctx.addMessage('system', `\u2713 Coordination ${parts[3]} cancelled.`);
2787
- }
2788
- else {
2789
- ctx.addMessage('system', `Coordination session not found: ${parts[3]}`);
2790
- }
2791
- }
2792
- else if (coordSubCmd && !['help'].includes(coordSubCmd)) {
2793
- // /swarm coordinate <objective>
2794
- const prompt = parts.slice(2).join(' ');
2795
- let cleanPrompt = prompt;
2796
- let template;
2797
- let mode = 'competitive';
2798
- const templateMatch = prompt.match(/--template\s+(\S+)/);
2799
- if (templateMatch) {
2800
- template = templateMatch[1];
2801
- cleanPrompt = cleanPrompt.replace(templateMatch[0], '').trim();
2802
- }
2803
- const modeMatch = prompt.match(/--mode\s+(competitive|collaborative|consensus|overseer)/);
2804
- if (modeMatch) {
2805
- mode = modeMatch[1];
2806
- cleanPrompt = cleanPrompt.replace(modeMatch[0], '').trim();
2807
- }
2808
- try {
2809
- let session;
2810
- if (template) {
2811
- session = await councilManager.startFromTemplate(template, cleanPrompt, swarmCwd);
2812
- }
2813
- else {
2814
- const { randomUUID } = await import('crypto');
2815
- const members = [
2816
- { id: randomUUID(), name: 'Agent A', agent: 'claude', weight: 1.0 },
2817
- { id: randomUUID(), name: 'Agent B', agent: 'claude', weight: 1.0 },
2818
- { id: randomUUID(), name: 'Agent C', agent: 'claude', weight: 1.0 },
2819
- ];
2820
- session = await councilManager.startCouncil(cleanPrompt, { mode, members }, swarmCwd);
2821
- }
2822
- watchAsyncLedgerRun(ctx.ledger, 'council', cleanPrompt, () => councilManager.getSession(session.id));
2823
- ctx.addMessage('system', `\u2713 Swarm coordination started: ${session.id.slice(0, 8)}\nMode: ${session.config.mode}\nAgents: ${session.config.members.map(m => m.name).join(', ')}\n\nUse /swarm coord status ${session.id.slice(0, 8)} to check progress.`);
2824
- }
2825
- catch (err) {
2826
- ctx.addMessage('error', `Failed to start coordination: ${err instanceof Error ? err.message : String(err)}`);
2827
- }
2828
- }
2829
- else {
2830
- ctx.addMessage('system', `Swarm Coordination: Multi-agent teams working toward shared objectives.
2831
-
2832
- Modes:
2833
- competitive — Agents work independently, best result wins
2834
- collaborative — Pipeline: each agent builds on previous work
2835
- consensus — Agents vote on approach, supermajority decides
2836
- overseer — Lead decomposes, delegates, reviews, coordinates
2837
-
2838
- Usage:
2839
- /swarm coord <objective> Start with default competitive mode
2840
- /swarm coord <objective> [options] Start with explicit options
2841
- /swarm coord status [id] Show session status
2842
- /swarm coord list List all sessions
2843
- /swarm coord templates Show available templates
2844
- /swarm coord cancel <id> Cancel a running session
2845
-
2846
- Options:
2847
- --template code-review|architecture|security-audit|brainstorm|debate
2848
- --mode competitive|collaborative|consensus|overseer`);
2849
- }
2850
- }
2851
- else if (subCmd === 'templates') {
2852
- // Shortcut: /swarm templates → show coordination templates
2853
- const templates = Object.values(COUNCIL_TEMPLATES);
2854
- let msg = 'Swarm Coordination Templates:\n';
2855
- for (const t of templates) {
2856
- msg += `\n ${t.name} - ${t.description}`;
2857
- msg += `\n Mode: ${t.mode}, Agents: ${t.members.map(m => m.name).join(', ')}`;
2858
- }
2859
- ctx.addMessage('system', msg);
2860
- }
2861
- else if (subCmd === 'start' || (subCmd && !['status', 'list', 'cancel', 'templates', 'coordinate', 'coord', 'help'].includes(subCmd))) {
2862
- // /swarm start <prompt> or /swarm <prompt> — task decomposition
2863
- const promptStart = subCmd === 'start' ? 2 : 1;
2864
- const prompt = parts.slice(promptStart).join(' ');
2865
- if (!prompt) {
2866
- ctx.addMessage('system', 'Usage: /swarm <task description>\n /swarm start <task> [--strategy parallel|sequential|map-reduce|pipeline] [--aggregation concatenate|merge-dedupe|summarize|structured]');
2867
- break;
2868
- }
2869
- let strategy = 'parallel';
2870
- let aggregation = 'concatenate';
2871
- let cleanPrompt = prompt;
2872
- const strategyMatch = prompt.match(/--strategy\s+(parallel|sequential|map-reduce|pipeline)/);
2873
- if (strategyMatch) {
2874
- strategy = strategyMatch[1];
2875
- cleanPrompt = cleanPrompt.replace(strategyMatch[0], '').trim();
2876
- }
2877
- const aggMatch = prompt.match(/--aggregation\s+(concatenate|merge-dedupe|summarize|structured)/);
2878
- if (aggMatch) {
2879
- aggregation = aggMatch[1];
2880
- cleanPrompt = cleanPrompt.replace(aggMatch[0], '').trim();
2881
- }
2882
- try {
2883
- const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation }, swarmCwd);
2884
- watchAsyncLedgerRun(ctx.ledger, 'swarm', cleanPrompt, () => swarmManager.getSession(session.id));
2885
- ctx.addMessage('system', `\u2713 Swarm started: ${session.id.slice(0, 8)}\nStrategy: ${strategy} \u2192 ${aggregation}\nStatus: ${session.status}\n\nUse /swarm status ${session.id.slice(0, 8)} to check progress.`);
2886
- }
2887
- catch (err) {
2888
- ctx.addMessage('error', `Failed to start swarm: ${err instanceof Error ? err.message : String(err)}`);
2889
- }
2890
- }
2891
- else if (subCmd === 'status') {
2892
- // Show both swarm and coordination sessions
2893
- const sessionId = parts[2];
2894
- if (sessionId) {
2895
- // Check decomposition swarms first, then coordination
2896
- const swarmSession = swarmManager.getAllSessions().find(s => s.id.startsWith(sessionId));
2897
- const coordSession = councilManager.getAllSessions().find(s => s.id.startsWith(sessionId));
2898
- if (swarmSession) {
2899
- let msg = swarmManager.formatSessionStatus(swarmSession);
2900
- if (swarmSession.status === 'completed' && swarmSession.result) {
2901
- msg += `\n\nResult:\n${swarmSession.result.slice(0, 500)}${(swarmSession.result.length > 500) ? '...' : ''}`;
2902
- }
2903
- ctx.addMessage('system', msg);
2904
- }
2905
- else if (coordSession) {
2906
- let msg = councilManager.formatSessionStatus(coordSession);
2907
- if (coordSession.status === 'completed' && coordSession.result) {
2908
- msg += `\n\nResult:\n${coordSession.result.slice(0, 500)}${(coordSession.result.length > 500) ? '...' : ''}`;
2909
- }
2910
- ctx.addMessage('system', msg);
2911
- }
2912
- else {
2913
- ctx.addMessage('system', `Swarm session not found: ${sessionId}`);
2914
- }
2915
- }
2916
- else {
2917
- const swarmSessions = swarmManager.getAllSessions();
2918
- const coordSessions = councilManager.getAllSessions();
2919
- if (swarmSessions.length === 0 && coordSessions.length === 0) {
2920
- ctx.addMessage('system', 'No swarm sessions.');
2921
- }
2922
- else {
2923
- const lines = [];
2924
- for (const s of swarmSessions) {
2925
- const subtaskInfo = s.subtasks.length > 0
2926
- ? ` (${s.subtasks.filter(st => st.status === 'completed').length}/${s.subtasks.length} done)`
2927
- : '';
2928
- lines.push(` ${s.id.slice(0, 8)} [decompose] ${s.status}${subtaskInfo} - ${s.prompt.slice(0, 50)}`);
2929
- }
2930
- for (const s of coordSessions) {
2931
- lines.push(` ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 50)}`);
2932
- }
2933
- ctx.addMessage('system', `Swarm Sessions:\n${lines.join('\n')}`);
2934
- }
2935
- }
2936
- }
2937
- else if (subCmd === 'list') {
2938
- const swarmSessions = swarmManager.getAllSessions();
2939
- const swarmStats = swarmManager.getStats();
2940
- const coordSessions = councilManager.getAllSessions();
2941
- const coordStats = councilManager.getStats();
2942
- let msg = `Decomposition: ${swarmStats.totalSessions} total, ${swarmStats.activeSessions} active, ${swarmStats.completedSessions} completed\n`;
2943
- msg += `Coordination: ${coordStats.totalSessions} total, ${coordStats.activeSessions} active, ${coordStats.completedSessions} completed\n`;
2944
- for (const s of swarmSessions) {
2945
- msg += `\n ${s.id.slice(0, 8)} [decompose] ${s.status} - ${s.prompt.slice(0, 60)}`;
2946
- }
2947
- for (const s of coordSessions) {
2948
- msg += `\n ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 60)}`;
2949
- }
2950
- ctx.addMessage('system', msg);
2951
- }
2952
- else if (subCmd === 'cancel' && parts[2]) {
2953
- const swarmSession = swarmManager.getAllSessions().find(s => s.id.startsWith(parts[2]));
2954
- const coordSession = councilManager.getAllSessions().find(s => s.id.startsWith(parts[2]));
2955
- if (swarmSession) {
2956
- await swarmManager.cancelSwarm(swarmSession.id);
2957
- ctx.addMessage('system', `\u2713 Swarm ${parts[2]} cancelled.`);
2958
- }
2959
- else if (coordSession) {
2960
- await councilManager.cancelCouncil(coordSession.id);
2961
- ctx.addMessage('system', `\u2713 Coordination ${parts[2]} cancelled.`);
2962
- }
2963
- else {
2964
- ctx.addMessage('system', `Swarm session not found: ${parts[2]}`);
2965
- }
2966
- }
2967
- else {
2968
- ctx.addMessage('system', `Agent Swarms: Decompose tasks and coordinate multi-agent teams.
2969
-
2970
- Decomposition (parallel workers):
2971
- /swarm <task> Decompose and execute in parallel
2972
- /swarm start <task> [options] Start with explicit strategy
2973
-
2974
- Coordination (multi-agent teamwork):
2975
- /swarm coord <objective> Start coordinated agents
2976
- /swarm coord <objective> [options] Start with explicit mode
2977
- /swarm coord templates Show coordination templates
2978
-
2979
- Management:
2980
- /swarm status [id] Show session status
2981
- /swarm list List all sessions
2982
- /swarm cancel <id> Cancel a session
2983
-
2984
- Decomposition options:
2985
- --strategy parallel|sequential|map-reduce|pipeline
2986
- --aggregation concatenate|merge-dedupe|summarize|structured
2987
-
2988
- Coordination options:
2989
- --mode competitive|collaborative|consensus|overseer
2990
- --template code-review|architecture|security-audit|brainstorm|debate
2991
-
2992
- Requires --agents flag.`);
2993
- }
2994
- break;
2995
- }
2996
- case '/checkpoint':
2997
- case '/cp': {
2998
- const { listCheckpoints, clearCheckpoints } = await import('../checkpoint.js');
2999
- const cpSubCmd = parts[1] || 'list';
3000
- if (cpSubCmd === 'list') {
3001
- const filterPath = parts[2];
3002
- const checkpoints = listCheckpoints(filterPath);
999
+ case '/restore': {
1000
+ const { restoreFromCheckpoint, listCheckpoints } = await import('../checkpoint.js');
1001
+ const restorePath = parts[1];
1002
+ if (!restorePath) {
1003
+ // No arg: list available checkpoints (absorbs the old /checkpoint list)
1004
+ const checkpoints = listCheckpoints();
3003
1005
  if (checkpoints.length === 0) {
3004
- ctx.addMessage('system', filterPath
3005
- ? `No checkpoints found for: ${filterPath}`
3006
- : 'No checkpoints found. Checkpoints are created automatically when files are overwritten.');
1006
+ ctx.addMessage('system', 'No checkpoints found. Checkpoints are created automatically (as git commits) before destructive tool calls, and require a git repository.');
3007
1007
  }
3008
1008
  else {
3009
1009
  const list = checkpoints.slice(0, 20).map((cp, i) => {
3010
- const relPath = path.relative(process.cwd(), cp.filePath);
3011
1010
  const time = new Date(cp.timestamp).toLocaleString();
3012
- const size = cp.size > 1024 ? `${(cp.size / 1024).toFixed(1)}KB` : `${cp.size}B`;
3013
- return ` ${i}. ${relPath} (${size}) - ${time}`;
1011
+ return ` ${i}. ${cp.hash} ${cp.subject} - ${time}`;
3014
1012
  }).join('\n');
3015
- ctx.addMessage('system', `Checkpoints (newest first):\n${list}${checkpoints.length > 20 ? `\n ... and ${checkpoints.length - 20} more` : ''}`);
1013
+ ctx.addMessage('system', `Checkpoints (newest first):\n${list}${checkpoints.length > 20 ? `\n ... and ${checkpoints.length - 20} more` : ''}\n\nUse /restore <path> [index] to restore a file.`);
3016
1014
  }
3017
- }
3018
- else if (cpSubCmd === 'clear') {
3019
- const days = parts[2] ? parseInt(parts[2]) : undefined;
3020
- const removed = clearCheckpoints(days);
3021
- ctx.addMessage('system', `Cleared ${removed} checkpoint${removed !== 1 ? 's' : ''}${days ? ` older than ${days} days` : ''}.`);
3022
- }
3023
- else {
3024
- ctx.addMessage('system', 'Usage: /checkpoint [list|clear]\n /checkpoint list [path] - list checkpoints\n /checkpoint clear [days] - clear old checkpoints\n /restore <path> [index] - restore a file');
3025
- }
3026
- break;
3027
- }
3028
- case '/restore': {
3029
- const { restoreCheckpoint, listCheckpoints } = await import('../checkpoint.js');
3030
- const restorePath = parts[1];
3031
- if (!restorePath) {
3032
- ctx.addMessage('error', 'Usage: /restore <path> [index]\n Restores a file from its most recent checkpoint.\n Use /checkpoint list to see available checkpoints.');
3033
1015
  break;
3034
1016
  }
3035
1017
  const idx = parts[2] ? parseInt(parts[2]) : 0;
@@ -3039,174 +1021,17 @@ Requires --agents flag.`);
3039
1021
  ctx.addMessage('error', `No checkpoints found for: ${restorePath}`);
3040
1022
  break;
3041
1023
  }
3042
- const restored = restoreCheckpoint(absRestorePath, idx);
1024
+ const restored = restoreFromCheckpoint(absRestorePath, idx);
3043
1025
  if (restored !== undefined) {
3044
1026
  const relPath = path.relative(process.cwd(), absRestorePath);
3045
1027
  const cp = checkpoints[idx];
3046
- ctx.addMessage('system', `✓ Restored ${relPath} from checkpoint (${new Date(cp.timestamp).toLocaleString()})`);
1028
+ ctx.addMessage('system', `✓ Restored ${relPath} from checkpoint ${cp.hash} (${new Date(cp.timestamp).toLocaleString()})`);
3047
1029
  }
3048
1030
  else {
3049
1031
  ctx.addMessage('error', `Failed to restore: checkpoint index ${idx} not found for ${restorePath}`);
3050
1032
  }
3051
1033
  break;
3052
1034
  }
3053
- // ================================================================
3054
- // Background Jobs
3055
- // ================================================================
3056
- case '/bg': {
3057
- const bgPrompt = parts.slice(1).join(' ');
3058
- if (!bgPrompt) {
3059
- ctx.addMessage('error', 'Usage: /bg <prompt> — run a task in the background');
3060
- break;
3061
- }
3062
- const bgJob = createJob(bgPrompt, { provider: ctx.actualProvider, model: ctx.actualModel });
3063
- ctx.addMessage('system', `Background job ${bgJob.id} created: "${bgPrompt.length > 60 ? bgPrompt.slice(0, 57) + '...' : bgPrompt}"`);
3064
- // Run the job using the agent
3065
- runJob(bgJob.id, async (prompt, signal) => {
3066
- const { chat } = await import('../providers/index.js');
3067
- const { TOOLS } = await import('../tools.js');
3068
- const { executeTool: execTool } = await import('../tools.js');
3069
- const cwd = ctx.sessionRef.current?.projectPath || process.cwd();
3070
- const maxIterations = resolveIterationLimit(config.get('maxIterations'));
3071
- const bgMessages = [
3072
- { role: 'system', content: buildFullSystemPrompt(ctx.persona, cwd) },
3073
- { role: 'user', content: prompt },
3074
- ];
3075
- let iterations = 0;
3076
- let lastContent = '';
3077
- while (!signal.aborted && iterations < maxIterations) {
3078
- iterations++;
3079
- const response = await chat(ctx.provider, bgMessages, TOOLS, ctx.model);
3080
- if (!response.toolCalls?.length) {
3081
- lastContent = response.content;
3082
- break;
3083
- }
3084
- bgMessages.push({ role: 'assistant', content: response.content, toolCalls: response.toolCalls });
3085
- for (const tc of response.toolCalls) {
3086
- const result = await execTool(tc, cwd);
3087
- bgMessages.push({ role: 'tool', content: result.result, toolCallId: tc.id });
3088
- if (signal.aborted)
3089
- break;
3090
- }
3091
- }
3092
- if (signal.aborted) {
3093
- const error = new Error('Background job cancelled');
3094
- error.name = 'AbortError';
3095
- throw error;
3096
- }
3097
- return { result: lastContent, iterations };
3098
- }).then(completed => {
3099
- ctx.addMessage('system', `Background job ${completed.id} ${completed.status}: ${completed.result?.slice(0, 200) || completed.error || 'done'}`);
3100
- }).catch(() => { });
3101
- break;
3102
- }
3103
- case '/jobs': {
3104
- ctx.addMessage('system', formatJobsList());
3105
- break;
3106
- }
3107
- case '/job': {
3108
- const jobId = parts[1];
3109
- if (!jobId) {
3110
- ctx.addMessage('error', 'Usage: /job <id> — show job details');
3111
- break;
3112
- }
3113
- const jobInfo = getJob(jobId);
3114
- if (!jobInfo) {
3115
- ctx.addMessage('error', `Job "${jobId}" not found.`);
3116
- }
3117
- else {
3118
- ctx.addMessage('system', formatJob(jobInfo) + (jobInfo.result ? `\n\nResult:\n${jobInfo.result.slice(0, 2000)}` : ''));
3119
- }
3120
- break;
3121
- }
3122
- case '/cancel': {
3123
- const cancelId = parts[1];
3124
- if (!cancelId) {
3125
- ctx.addMessage('error', 'Usage: /cancel <job-id>');
3126
- break;
3127
- }
3128
- const cancelled = cancelJob(cancelId);
3129
- ctx.addMessage('system', cancelled ? `Cancelled job ${cancelId}` : `Could not cancel ${cancelId} (not running or not found)`);
3130
- break;
3131
- }
3132
- case '/clear-jobs': {
3133
- const cleared = clearFinishedJobs();
3134
- ctx.addMessage('system', cleared > 0 ? `Cleared ${cleared} finished job(s).` : 'No finished jobs to clear.');
3135
- break;
3136
- }
3137
- // ================================================================
3138
- // Recordings
3139
- // ================================================================
3140
- case '/recordings': {
3141
- const recs = listRecordings();
3142
- if (recs.length === 0) {
3143
- ctx.addMessage('system', 'No recordings found.');
3144
- }
3145
- else {
3146
- const lines = recs.slice(0, 20).map(r => {
3147
- const dur = r.duration > 0 ? ` (${Math.floor(r.duration / 60000)}m${Math.floor((r.duration % 60000) / 1000)}s)` : '';
3148
- return ` ${r.id} ${r.startTime} ${r.eventCount} events${dur}`;
3149
- });
3150
- ctx.addMessage('system', `Recordings (${recs.length} total):\n${lines.join('\n')}`);
3151
- }
3152
- break;
3153
- }
3154
- case '/recording': {
3155
- const recId = parts[1];
3156
- if (!recId) {
3157
- ctx.addMessage('error', 'Usage: /recording <id> — show recording details');
3158
- break;
3159
- }
3160
- const rec = loadRecording(recId);
3161
- if (!rec) {
3162
- ctx.addMessage('error', `Recording "${recId}" not found.`);
3163
- }
3164
- else {
3165
- ctx.addMessage('system', formatRecording(rec));
3166
- }
3167
- break;
3168
- }
3169
- case '/delete-recording': {
3170
- const delRecId = parts[1];
3171
- if (!delRecId) {
3172
- ctx.addMessage('error', 'Usage: /delete-recording <id>');
3173
- break;
3174
- }
3175
- const delResult = deleteRecording(delRecId);
3176
- ctx.addMessage('system', delResult ? `Deleted recording ${delRecId}` : `Recording "${delRecId}" not found.`);
3177
- break;
3178
- }
3179
- // ================================================================
3180
- // API Server
3181
- // ================================================================
3182
- case '/serve': {
3183
- if (isApiServerRunning()) {
3184
- ctx.addMessage('system', 'API server is already running.');
3185
- break;
3186
- }
3187
- const servePort = parts[1] ? parseInt(parts[1], 10) : 3100;
3188
- if (isNaN(servePort) || servePort < 1 || servePort > 65535) {
3189
- ctx.addMessage('error', 'Invalid port. Usage: /serve [port]');
3190
- break;
3191
- }
3192
- try {
3193
- const info = await startApiServer({ port: servePort, host: '127.0.0.1' });
3194
- ctx.addMessage('system', `API server started on http://${info.host}:${info.port}`);
3195
- }
3196
- catch (err) {
3197
- ctx.addMessage('error', `Failed to start API server: ${err instanceof Error ? err.message : String(err)}`);
3198
- }
3199
- break;
3200
- }
3201
- case '/serve-stop': {
3202
- if (!isApiServerRunning()) {
3203
- ctx.addMessage('system', 'API server is not running.');
3204
- break;
3205
- }
3206
- await stopApiServer();
3207
- ctx.addMessage('system', 'API server stopped.');
3208
- break;
3209
- }
3210
1035
  case '/exit':
3211
1036
  case '/quit':
3212
1037
  process.exit(0);