@calliopelabs/cli 2.5.0 → 3.1.0

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