@calliopelabs/cli 0.8.20 → 2.0.2

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