aicodeman 1.35.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +33 -7
  2. package/dist/attachment-registry.d.ts +6 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +9 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +225 -0
  7. package/dist/cli-agent.d.ts.map +1 -0
  8. package/dist/cli-agent.js +767 -0
  9. package/dist/cli-agent.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +8 -32
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codeman-credentials.d.ts +27 -0
  14. package/dist/codeman-credentials.d.ts.map +1 -0
  15. package/dist/codeman-credentials.js +66 -0
  16. package/dist/codeman-credentials.js.map +1 -0
  17. package/dist/config/cli-registry/patterns.d.ts +6 -0
  18. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  19. package/dist/config/cli-registry/patterns.js +13 -0
  20. package/dist/config/cli-registry/patterns.js.map +1 -1
  21. package/dist/config/cli-registry/schema.d.ts +15 -0
  22. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  23. package/dist/config/cli-registry/schema.js +63 -2
  24. package/dist/config/cli-registry/schema.js.map +1 -1
  25. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  26. package/dist/config/cli-registry/stock.js +180 -7
  27. package/dist/config/cli-registry/stock.js.map +1 -1
  28. package/dist/config/cli-registry/types.d.ts +105 -7
  29. package/dist/config/cli-registry/types.d.ts.map +1 -1
  30. package/dist/deepseek-route-config.d.ts +110 -0
  31. package/dist/deepseek-route-config.d.ts.map +1 -0
  32. package/dist/deepseek-route-config.js +477 -0
  33. package/dist/deepseek-route-config.js.map +1 -0
  34. package/dist/git-clone.d.ts.map +1 -1
  35. package/dist/git-clone.js +4 -0
  36. package/dist/git-clone.js.map +1 -1
  37. package/dist/git-workspace-status.d.ts +31 -5
  38. package/dist/git-workspace-status.d.ts.map +1 -1
  39. package/dist/git-workspace-status.js +49 -29
  40. package/dist/git-workspace-status.js.map +1 -1
  41. package/dist/image-watcher.d.ts.map +1 -1
  42. package/dist/image-watcher.js +6 -2
  43. package/dist/image-watcher.js.map +1 -1
  44. package/dist/mcp-sync-targets.d.ts +34 -0
  45. package/dist/mcp-sync-targets.d.ts.map +1 -0
  46. package/dist/mcp-sync-targets.js +55 -0
  47. package/dist/mcp-sync-targets.js.map +1 -0
  48. package/dist/mcp-sync.d.ts +2 -1
  49. package/dist/mcp-sync.d.ts.map +1 -1
  50. package/dist/mcp-sync.js +62 -1
  51. package/dist/mcp-sync.js.map +1 -1
  52. package/dist/model-config-resolvers.d.ts +29 -0
  53. package/dist/model-config-resolvers.d.ts.map +1 -0
  54. package/dist/model-config-resolvers.js +34 -0
  55. package/dist/model-config-resolvers.js.map +1 -0
  56. package/dist/session-display-model.d.ts +110 -0
  57. package/dist/session-display-model.d.ts.map +1 -0
  58. package/dist/session-display-model.js +154 -0
  59. package/dist/session-display-model.js.map +1 -0
  60. package/dist/session.d.ts +145 -8
  61. package/dist/session.d.ts.map +1 -1
  62. package/dist/session.js +290 -39
  63. package/dist/session.js.map +1 -1
  64. package/dist/subagent-watcher.d.ts.map +1 -1
  65. package/dist/subagent-watcher.js +29 -6
  66. package/dist/subagent-watcher.js.map +1 -1
  67. package/dist/templates/case-template.md +2 -0
  68. package/dist/tmux-manager.d.ts +5 -0
  69. package/dist/tmux-manager.d.ts.map +1 -1
  70. package/dist/tmux-manager.js +47 -22
  71. package/dist/tmux-manager.js.map +1 -1
  72. package/dist/tui/tui-client.d.ts +3 -17
  73. package/dist/tui/tui-client.d.ts.map +1 -1
  74. package/dist/tui/tui-client.js +3 -44
  75. package/dist/tui/tui-client.js.map +1 -1
  76. package/dist/types/session.d.ts +28 -1
  77. package/dist/types/session.d.ts.map +1 -1
  78. package/dist/types/session.js.map +1 -1
  79. package/dist/types/tools.d.ts +1 -1
  80. package/dist/types/tools.d.ts.map +1 -1
  81. package/dist/utils/deepseek-cli-resolver.d.ts +8 -0
  82. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
  83. package/dist/utils/deepseek-cli-resolver.js +18 -2
  84. package/dist/utils/deepseek-cli-resolver.js.map +1 -1
  85. package/dist/utils/index.d.ts +1 -0
  86. package/dist/utils/index.d.ts.map +1 -1
  87. package/dist/utils/index.js +1 -0
  88. package/dist/utils/index.js.map +1 -1
  89. package/dist/utils/process-exit-wait.d.ts +40 -0
  90. package/dist/utils/process-exit-wait.d.ts.map +1 -0
  91. package/dist/utils/process-exit-wait.js +68 -0
  92. package/dist/utils/process-exit-wait.js.map +1 -0
  93. package/dist/web/launch-defaults.d.ts +22 -0
  94. package/dist/web/launch-defaults.d.ts.map +1 -0
  95. package/dist/web/launch-defaults.js +46 -0
  96. package/dist/web/launch-defaults.js.map +1 -0
  97. package/dist/web/paste-image-gc.d.ts +46 -6
  98. package/dist/web/paste-image-gc.d.ts.map +1 -1
  99. package/dist/web/paste-image-gc.js +106 -32
  100. package/dist/web/paste-image-gc.js.map +1 -1
  101. package/dist/web/public/admin-ui.js.gz +0 -0
  102. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  103. package/dist/web/public/app.6f8552e1.js +43 -0
  104. package/dist/web/public/app.6f8552e1.js.br +0 -0
  105. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  106. package/dist/web/public/approvals-ui.js.gz +0 -0
  107. package/dist/web/public/{constants.d95fade9.js → constants.bc9c1f63.js} +1276 -127
  108. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  109. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  110. package/dist/web/public/cron-ui.js.gz +0 -0
  111. package/dist/web/public/entrance-animations.js +491 -43
  112. package/dist/web/public/entrance-animations.js.br +0 -0
  113. package/dist/web/public/entrance-animations.js.gz +0 -0
  114. package/dist/web/public/git-status-ui.js +55 -9
  115. package/dist/web/public/git-status-ui.js.br +0 -0
  116. package/dist/web/public/git-status-ui.js.gz +0 -0
  117. package/dist/web/public/home-sessions.js +13 -9
  118. package/dist/web/public/home-sessions.js.br +0 -0
  119. package/dist/web/public/home-sessions.js.gz +0 -0
  120. package/dist/web/public/host-wake-ui.js.gz +0 -0
  121. package/dist/web/public/i18n.a9840ea2.js +2 -0
  122. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  123. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  124. package/dist/web/public/{image-input.a216cf9e.js → image-input.e00471c6.js} +21 -9
  125. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  126. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  127. package/dist/web/public/index.html +250 -59
  128. package/dist/web/public/index.html.br +0 -0
  129. package/dist/web/public/index.html.gz +0 -0
  130. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  131. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  132. package/dist/web/public/{mobile-handlers.23a85db1.js → mobile-handlers.0ef9c311.js} +20 -0
  133. package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
  134. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  135. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  136. package/dist/web/public/mobile-overview.js.gz +0 -0
  137. package/dist/web/public/mobile.111b187d.css +1 -0
  138. package/dist/web/public/mobile.111b187d.css.br +0 -0
  139. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  140. package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
  141. package/dist/web/public/notification-manager.feb82c04.js.br +0 -0
  142. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  143. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  144. package/dist/web/public/{panels-ui.6a5898d9.js → panels-ui.71846fcb.js} +4 -4
  145. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  146. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  147. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  148. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  149. package/dist/web/public/readmymind-ui.js.gz +0 -0
  150. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  151. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  152. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  153. package/dist/web/public/session-lineage.js +229 -86
  154. package/dist/web/public/session-lineage.js.br +0 -0
  155. package/dist/web/public/session-lineage.js.gz +0 -0
  156. package/dist/web/public/session-ui.f731602c.js +100 -0
  157. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  158. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  159. package/dist/web/public/settings-ui.4e33390f.js +97 -0
  160. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  161. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  162. package/dist/web/public/spreadsheet-preview-worker.js +290 -0
  163. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  164. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  165. package/dist/web/public/spreadsheet-preview.af82fa2f.js +555 -0
  166. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  167. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  168. package/dist/web/public/spreadsheet-xlsx-core.js +1006 -0
  169. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  170. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  171. package/dist/web/public/styles.81a72906.css +1 -0
  172. package/dist/web/public/styles.81a72906.css.br +0 -0
  173. package/dist/web/public/styles.81a72906.css.gz +0 -0
  174. package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
  175. package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
  176. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  177. package/dist/web/public/sw.js +2 -2
  178. package/dist/web/public/sw.js.br +0 -0
  179. package/dist/web/public/sw.js.gz +0 -0
  180. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  181. package/dist/web/public/{tab-rail-resize.94cd59f0.js → tab-rail-resize.a5867793.js} +1 -1
  182. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  183. package/dist/web/public/{tab-rail-resize.94cd59f0.js.gz → tab-rail-resize.a5867793.js.gz} +0 -0
  184. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +1 -0
  185. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  186. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  187. package/dist/web/public/terminal-split.js +146 -649
  188. package/dist/web/public/terminal-split.js.br +0 -0
  189. package/dist/web/public/terminal-split.js.gz +0 -0
  190. package/dist/web/public/terminal-tile.js +1688 -0
  191. package/dist/web/public/terminal-tile.js.br +0 -0
  192. package/dist/web/public/terminal-tile.js.gz +0 -0
  193. package/dist/web/public/terminal-ui.b7267005.js +2 -0
  194. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  195. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
  196. package/dist/web/public/tile-grid.js +2895 -0
  197. package/dist/web/public/tile-grid.js.br +0 -0
  198. package/dist/web/public/tile-grid.js.gz +0 -0
  199. package/dist/web/public/ultracode-panel.js.gz +0 -0
  200. package/dist/web/public/ultracode-windows.js +9 -6
  201. package/dist/web/public/ultracode-windows.js.br +0 -0
  202. package/dist/web/public/ultracode-windows.js.gz +0 -0
  203. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  204. package/dist/web/public/vendor/exceljs.min.js +44 -0
  205. package/dist/web/public/vendor/exceljs.min.js.br +0 -0
  206. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  207. package/dist/web/public/vendor/fflate.min.js +1 -0
  208. package/dist/web/public/vendor/fflate.min.js.br +0 -0
  209. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  210. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  211. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  213. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  214. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  215. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  216. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
  217. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  218. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  219. package/dist/web/public/vendor/xterm.css.gz +0 -0
  220. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  221. package/dist/web/public/{voice-input.c4b51eb6.js → voice-input.2342c37b.js} +64 -10
  222. package/dist/web/public/voice-input.2342c37b.js.br +0 -0
  223. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  224. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  225. package/dist/web/public/webview-tabs.js +9 -1
  226. package/dist/web/public/webview-tabs.js.br +0 -0
  227. package/dist/web/public/webview-tabs.js.gz +0 -0
  228. package/dist/web/routes/cli-registry-routes.d.ts +23 -4
  229. package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
  230. package/dist/web/routes/cli-registry-routes.js +95 -8
  231. package/dist/web/routes/cli-registry-routes.js.map +1 -1
  232. package/dist/web/routes/file-routes.d.ts +7 -0
  233. package/dist/web/routes/file-routes.d.ts.map +1 -1
  234. package/dist/web/routes/file-routes.js +34 -4
  235. package/dist/web/routes/file-routes.js.map +1 -1
  236. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  237. package/dist/web/routes/git-status-routes.js +33 -6
  238. package/dist/web/routes/git-status-routes.js.map +1 -1
  239. package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
  240. package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
  241. package/dist/web/routes/mcp-sync-routes.js +10 -3
  242. package/dist/web/routes/mcp-sync-routes.js.map +1 -1
  243. package/dist/web/routes/reboot-restore-routes.js +1 -1
  244. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  245. package/dist/web/routes/session-routes.d.ts +18 -0
  246. package/dist/web/routes/session-routes.d.ts.map +1 -1
  247. package/dist/web/routes/session-routes.js +154 -69
  248. package/dist/web/routes/session-routes.js.map +1 -1
  249. package/dist/web/routes/status-telemetry-routes.d.ts +3 -0
  250. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  251. package/dist/web/routes/status-telemetry-routes.js +12 -0
  252. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  253. package/dist/web/routes/system-routes.d.ts.map +1 -1
  254. package/dist/web/routes/system-routes.js +13 -0
  255. package/dist/web/routes/system-routes.js.map +1 -1
  256. package/dist/web/schemas.d.ts +30 -0
  257. package/dist/web/schemas.d.ts.map +1 -1
  258. package/dist/web/schemas.js +43 -0
  259. package/dist/web/schemas.js.map +1 -1
  260. package/dist/web/server.d.ts +6 -1
  261. package/dist/web/server.d.ts.map +1 -1
  262. package/dist/web/server.js +47 -21
  263. package/dist/web/server.js.map +1 -1
  264. package/dist/web/session-listener-wiring.d.ts +1 -0
  265. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  266. package/dist/web/session-listener-wiring.js +11 -0
  267. package/dist/web/session-listener-wiring.js.map +1 -1
  268. package/package.json +6 -4
  269. package/scripts/postinstall.js +16 -0
  270. package/skills/codeman/SKILL.md +6 -0
  271. package/README.zh-CN.md +0 -1139
  272. package/dist/web/public/app.b9516c7b.js +0 -43
  273. package/dist/web/public/app.b9516c7b.js.br +0 -0
  274. package/dist/web/public/app.b9516c7b.js.gz +0 -0
  275. package/dist/web/public/constants.d95fade9.js.br +0 -0
  276. package/dist/web/public/constants.d95fade9.js.gz +0 -0
  277. package/dist/web/public/i18n.52f5b7cc.js +0 -1
  278. package/dist/web/public/i18n.52f5b7cc.js.br +0 -0
  279. package/dist/web/public/i18n.52f5b7cc.js.gz +0 -0
  280. package/dist/web/public/image-input.a216cf9e.js.br +0 -0
  281. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  282. package/dist/web/public/mobile-handlers.23a85db1.js.br +0 -0
  283. package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
  284. package/dist/web/public/mobile.1aa4952d.css +0 -1
  285. package/dist/web/public/mobile.1aa4952d.css.br +0 -0
  286. package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
  287. package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
  288. package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
  289. package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
  290. package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
  291. package/dist/web/public/session-ui.6bddf916.js +0 -89
  292. package/dist/web/public/session-ui.6bddf916.js.br +0 -0
  293. package/dist/web/public/session-ui.6bddf916.js.gz +0 -0
  294. package/dist/web/public/settings-ui.6e798b7b.js +0 -97
  295. package/dist/web/public/settings-ui.6e798b7b.js.br +0 -0
  296. package/dist/web/public/settings-ui.6e798b7b.js.gz +0 -0
  297. package/dist/web/public/styles.2d175c60.css +0 -1
  298. package/dist/web/public/styles.2d175c60.css.br +0 -0
  299. package/dist/web/public/styles.2d175c60.css.gz +0 -0
  300. package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
  301. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  302. package/dist/web/public/tab-rail-resize.94cd59f0.js.br +0 -0
  303. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +0 -1
  304. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
  305. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  306. package/dist/web/public/terminal-ui.a4535311.js +0 -2
  307. package/dist/web/public/terminal-ui.a4535311.js.br +0 -0
  308. package/dist/web/public/terminal-ui.a4535311.js.gz +0 -0
  309. package/dist/web/public/upload.html +0 -155
  310. package/dist/web/public/upload.html.br +0 -0
  311. package/dist/web/public/upload.html.gz +0 -0
  312. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
  313. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
  314. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
  315. package/dist/web/public/voice-input.c4b51eb6.js.br +0 -0
  316. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
@@ -0,0 +1,767 @@
1
+ /**
2
+ * @fileoverview `codeman agent …` — session-to-session verbs for the agent running
3
+ * inside a Codeman session, in every CLI mode.
4
+ *
5
+ * A thin HTTP client over endpoints that already exist (`quick-start`, `input`,
6
+ * `wait`, `wait-output`, `last-response`, `terminal`, `DELETE sessions/:id`). It
7
+ * invents no route and no transport: everything goes through `CODEMAN_API_URL`, so
8
+ * auth, ownership and the per-session waiter cap apply unchanged. The behaviour is
9
+ * the packaged agent skill's (`skills/codeman`), ported from shell prose into code
10
+ * with tests, so a `codex`/`opencode`/`pi` agent — which never gets the claude-only
11
+ * preamble — has the same verbs from one line in its AGENTS.md.
12
+ *
13
+ * Invariants (each asserted in `test/cli-agent.test.ts`):
14
+ * 1. Refuses outside a Codeman session (`CODEMAN_MUX=1` + `CODEMAN_API_URL`); it
15
+ * never guesses a URL — a server you are not part of is not yours to drive.
16
+ * 2. `send` transmits printable text plus `\r` only. ESC exists solely as
17
+ * `interrupt`, which never appends `\r`. A stray control byte is a dead session
18
+ * in the fullscreen TUIs (opencode's `Ctrl+C` is `app_exit`).
19
+ * 3. `rm` fails closed: empty id, a short self id, or a prefix match in EITHER
20
+ * direction refuses. Ids appear in full and 8-char form, so equality alone
21
+ * misses a real combination — and the miss deletes the caller.
22
+ * 4. An id shorter than 8 characters refuses (exit 4) before any request, on every
23
+ * verb. `9` would resolve to whichever session is alone with that first
24
+ * character, the user's own interactive tab included.
25
+ *
26
+ * Commands live here as functions returning an exit code, not calling
27
+ * `process.exit`, so the whole surface is unit-testable against a fake server.
28
+ *
29
+ * @module cli-agent
30
+ */
31
+ import http from 'node:http';
32
+ import https from 'node:https';
33
+ import { basicAuthHeader, credentialsFrom, readCodemanEnvFile, } from './codeman-credentials.js';
34
+ import { getCli } from './config/cli-registry/registry.js';
35
+ import { GLYPH, palette, table } from './cli-style.js';
36
+ import { getErrorMessage } from './types.js';
37
+ import { stripAnsi as stripAnsiSequences } from './utils/regex-patterns.js';
38
+ /** Thrown when the process is not inside a Codeman-managed session. */
39
+ export class AgentGuardError extends Error {
40
+ }
41
+ /** Exit codes shared by every verb; a shell agent can branch on them. */
42
+ export const EXIT = {
43
+ ok: 0,
44
+ error: 1,
45
+ timeout: 2,
46
+ dead: 3,
47
+ refused: 4,
48
+ };
49
+ /**
50
+ * Resolve the context from the environment, or throw `AgentGuardError`.
51
+ *
52
+ * Credentials in the order every client of the API uses (`credentialsFrom`, shared
53
+ * with `codeman attach` and the TUI): each field from the environment (a session
54
+ * inherits the server's), then the data dir's `.env`. No password means the server
55
+ * is open (single-user) — or it is not, and the 401 says so.
56
+ */
57
+ export function resolveAgentContext(env = process.env, envFile = readCodemanEnvFile) {
58
+ if (env.CODEMAN_MUX !== '1') {
59
+ throw new AgentGuardError('Not inside a Codeman-managed session (CODEMAN_MUX is not 1); refusing to act.');
60
+ }
61
+ const apiUrl = env.CODEMAN_API_URL?.trim();
62
+ if (!apiUrl) {
63
+ throw new AgentGuardError('CODEMAN_API_URL is not set; refusing to guess a server.');
64
+ }
65
+ const selfId = env.CODEMAN_SESSION_ID?.trim();
66
+ if (!selfId) {
67
+ throw new AgentGuardError('CODEMAN_SESSION_ID is not set; cannot tell which session is me.');
68
+ }
69
+ const credentials = credentialsFrom(env, envFile());
70
+ return { apiUrl, selfId, auth: credentials.password ? credentials : undefined };
71
+ }
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+ // Pure helpers (the invariants)
74
+ // ─────────────────────────────────────────────────────────────────────────────
75
+ /**
76
+ * Is `id` this session? Prefix in BOTH directions, because ids appear in full and
77
+ * in 8-char form (mux names, UI surfaces, Docker's truncated `$SELF`). A self id
78
+ * shorter than 8 characters cannot prove anything and is treated as "maybe me".
79
+ */
80
+ export function isSelfSession(selfId, id) {
81
+ if (!id || selfId.length < 8)
82
+ return true;
83
+ return id.startsWith(selfId) || selfId.startsWith(id);
84
+ }
85
+ /** Why `rm` refuses, or `undefined` when the delete may go ahead. */
86
+ export function deleteRefusal(selfId, id) {
87
+ if (!id)
88
+ return 'refusing: empty session id';
89
+ if (selfId.length < 8)
90
+ return 'refusing: own session id unset or too short to prove this is not me';
91
+ if (isSelfSession(selfId, id))
92
+ return `refusing: ${id} is me`;
93
+ return undefined;
94
+ }
95
+ /**
96
+ * The prompt `send` was given, which must be ONE argument. Joining several with spaces
97
+ * would turn an unquoted `$(cat notes.txt)`, which the shell splits on every newline,
98
+ * back into a single line, so the multi-line refusal below would never see it.
99
+ */
100
+ export function sendPromptFromArgs(words) {
101
+ if (words.length === 1)
102
+ return { text: words[0] };
103
+ return {
104
+ error: `refusing: the prompt must be ONE argument, got ${words.length} — quote it (\`send <id> "…"\`; a prompt that starts with "-" goes after --: \`send <id> -- "- fix the bug"\`)`,
105
+ };
106
+ }
107
+ /**
108
+ * Why `send` refuses this text, or `undefined` when it is printable. The composer
109
+ * takes one line; the server strips `\r`/`\n` but everything else below 0x20 (and
110
+ * DEL) reaches the pane as a keypress. None of that is a prompt.
111
+ */
112
+ export function inputRefusal(text) {
113
+ if (text.length === 0)
114
+ return 'refusing: empty input (use `interrupt` for ESC, `send <id> ""` is never a prompt)';
115
+ // The composer is one line: the server strips newlines, which silently joins the
116
+ // lines into one prompt, and a tab reaches the pane as a keypress (claude: mode toggle).
117
+ if (/[\n\r\t]/.test(text)) {
118
+ return 'refusing: input must be a single line (the composer strips newlines and would join your lines) — join them yourself, or write a file into the workspace and send its path';
119
+ }
120
+ // C0, DEL and C1 (U+0080–U+009F: an 8-bit CSI is still a CSI to a terminal).
121
+ // eslint-disable-next-line no-control-regex
122
+ const control = text.match(/[\x00-\x1f\x7f-\x9f]/);
123
+ if (control) {
124
+ const code = control[0].charCodeAt(0).toString(16).padStart(2, '0');
125
+ return `refusing: input contains control byte 0x${code}; send transmits printable text only (ESC is \`interrupt\`)`;
126
+ }
127
+ return undefined;
128
+ }
129
+ /**
130
+ * Body for `POST /sessions/:id/input` on the send path: text plus `\r` unless the
131
+ * caller asked to type without submitting. Never anything else.
132
+ */
133
+ export function buildSendBody(text, options) {
134
+ const body = {
135
+ input: options.enter ? `${text}\r` : text,
136
+ useMux: true,
137
+ clientId: options.clientId,
138
+ seq: options.seq,
139
+ };
140
+ if (options.wait !== undefined)
141
+ body.wait = options.wait;
142
+ if (options.waitTimeout !== undefined)
143
+ body.waitTimeout = options.waitTimeout;
144
+ return body;
145
+ }
146
+ /** Body for the interrupt path: a bare ESC, and nothing appended — ever. */
147
+ export function buildInterruptBody(clientId, seq) {
148
+ return { input: '\u001b', useMux: true, clientId, seq };
149
+ }
150
+ /** `clientId` for this caller: fixed per sending session, so `seq` stays monotonic. */
151
+ export function defaultClientId(selfId, suffix = '') {
152
+ return `codeman-agent-cli-${selfId.slice(0, 8)}${suffix ? `-${suffix}` : ''}`;
153
+ }
154
+ /**
155
+ * Strip a terminal buffer for humans: the shared ANSI strip (CSI, OSC such as window
156
+ * titles, keypad modes) plus the charset designators (`ESC ( B`) it leaves in.
157
+ */
158
+ export function stripAnsi(text) {
159
+ // eslint-disable-next-line no-control-regex
160
+ return stripAnsiSequences(text).replace(/\x1b[()][AB0]/g, '');
161
+ }
162
+ /** Parse a positive-integer option (`--timeout` ms, `--tail` bytes); the server rejects anything else. */
163
+ export function parsePositiveInt(raw, fallback, flag = '--timeout') {
164
+ if (raw === undefined)
165
+ return fallback;
166
+ const n = Number(raw);
167
+ if (!Number.isInteger(n) || n <= 0) {
168
+ throw new Error(`${flag} must be a positive integer, got "${raw}"`);
169
+ }
170
+ return n;
171
+ }
172
+ /**
173
+ * Exit code for a wait result: matched or a signal → ok, `exit` or `ended` → dead,
174
+ * timeout → timeout. `ended` is checked BEFORE the happy paths: a worker that dies
175
+ * during `--until stop` comes back as `ended:true, signal:null` (the registry only
176
+ * satisfies waiters that listed `exit`, then cancels the rest), and a `--match` on
177
+ * a dead worker as `ended:true, matched:false` — both are "dead", never "done".
178
+ */
179
+ export function waitExitCode(wait) {
180
+ if (!wait)
181
+ return EXIT.error;
182
+ if (wait.signal === 'exit' || wait.ended)
183
+ return EXIT.dead;
184
+ if (wait.timedOut)
185
+ return EXIT.timeout;
186
+ if (wait.matched === false)
187
+ return EXIT.timeout;
188
+ return EXIT.ok;
189
+ }
190
+ /** Every request carries these; they are ignored on endpoints that do not read them. */
191
+ export function baseHeaders(ctx) {
192
+ const headers = {
193
+ Accept: 'application/json',
194
+ // Tags sessions this caller spawns as its children (lineage in the web UI).
195
+ // Cosmetic, never fails a call. NOT X-Codeman-Agent-Origin: that one marks a case
196
+ // directory as deletable agent scratch, so it rides only the spawn request that
197
+ // may create one (see agentSpawn), never anything else.
198
+ 'X-Codeman-Parent-Session': ctx.selfId,
199
+ };
200
+ const authorization = ctx.auth ? basicAuthHeader(ctx.auth) : undefined;
201
+ if (authorization)
202
+ headers.Authorization = authorization;
203
+ return headers;
204
+ }
205
+ /** The real transport. `rejectUnauthorized:false` because the HTTPS install uses a self-signed cert. */
206
+ export const httpRequest = (ctx, options) => {
207
+ const url = new URL(options.path, ctx.apiUrl);
208
+ for (const [key, value] of Object.entries(options.query ?? {})) {
209
+ if (value !== undefined)
210
+ url.searchParams.set(key, String(value));
211
+ }
212
+ const bodyText = options.body === undefined ? undefined : JSON.stringify(options.body);
213
+ const headers = { ...baseHeaders(ctx), ...(options.headers ?? {}) };
214
+ if (bodyText !== undefined) {
215
+ headers['Content-Type'] = 'application/json';
216
+ headers['Content-Length'] = Buffer.byteLength(bodyText);
217
+ }
218
+ const transport = url.protocol === 'https:' ? https : http;
219
+ return new Promise((resolve, reject) => {
220
+ const req = transport.request({
221
+ protocol: url.protocol,
222
+ hostname: url.hostname,
223
+ port: url.port,
224
+ method: options.method,
225
+ path: `${url.pathname}${url.search}`,
226
+ rejectUnauthorized: false,
227
+ headers,
228
+ timeout: options.timeoutMs ?? 30_000,
229
+ }, (res) => {
230
+ const chunks = [];
231
+ res.on('data', (chunk) => chunks.push(chunk));
232
+ res.on('end', () => {
233
+ const text = Buffer.concat(chunks).toString('utf-8');
234
+ let json;
235
+ try {
236
+ json = JSON.parse(text);
237
+ }
238
+ catch {
239
+ json = undefined;
240
+ }
241
+ resolve({ status: res.statusCode ?? 0, json, text });
242
+ });
243
+ });
244
+ req.on('timeout', () => req.destroy(new Error(`request timed out after ${options.timeoutMs ?? 30_000} ms`)));
245
+ req.on('error', reject);
246
+ if (bodyText !== undefined)
247
+ req.write(bodyText);
248
+ req.end();
249
+ });
250
+ };
251
+ /** One line describing a failed response, for humans. Plain-text guards (401/403/429) have no envelope. */
252
+ export function describeFailure(res) {
253
+ if (res.json && !res.json.success) {
254
+ return `${res.json.errorCode ?? 'ERROR'}: ${res.json.error ?? 'request failed'} (HTTP ${res.status})`;
255
+ }
256
+ const text = res.text.trim().split('\n')[0] ?? '';
257
+ if (res.status === 401)
258
+ return `HTTP 401 ${text}: the server wants a password (CODEMAN_PASSWORD, or the data dir's .env)`;
259
+ return `HTTP ${res.status}${text ? ` ${text}` : ''}`;
260
+ }
261
+ /** Print `data` as JSON (the `--json` path) — always the envelope's `data`, never a reshaped copy. */
262
+ function emitJson(deps, data) {
263
+ deps.io.out(JSON.stringify(data, null, 2));
264
+ }
265
+ function fail(deps, message, code = EXIT.error) {
266
+ if (deps.json) {
267
+ deps.io.out(JSON.stringify({ success: false, error: message }));
268
+ }
269
+ else {
270
+ deps.io.err(palette.err(`${GLYPH.fail} ${message}`));
271
+ }
272
+ return code;
273
+ }
274
+ /** A full session id (the only form the routes accept); `ls` prints the 8-char prefix. */
275
+ const FULL_ID_LENGTH = 36;
276
+ /**
277
+ * Shortest prefix that may name a session: the 8-char form `ls` prints, and the floor
278
+ * the server's own resolver uses (`PARENT_SESSION_ID_MIN_PREFIX`, route-helpers.ts).
279
+ */
280
+ export const MIN_ID_PREFIX_LENGTH = 8;
281
+ /**
282
+ * Turn the id a human typed into the one the routes accept. `ls` prints 8-char
283
+ * prefixes and the routes answer 404 to those (measured live), so anything shorter
284
+ * than a full id resolves through the session list; an ambiguous prefix refuses
285
+ * rather than picking one. Below 8 characters it refuses before the list: "unique"
286
+ * means nothing for `9` — it names whatever session happens to be alone with that
287
+ * first character, and `rm`/`send` would act on it.
288
+ */
289
+ export async function resolveSessionId(deps, id) {
290
+ if (!id)
291
+ return { error: 'refusing: empty session id', code: EXIT.refused };
292
+ if (id.length < MIN_ID_PREFIX_LENGTH) {
293
+ return {
294
+ error: `refusing: "${id}" is shorter than ${MIN_ID_PREFIX_LENGTH} characters — use the 8-character id \`agent ls\` prints, or the full id`,
295
+ code: EXIT.refused,
296
+ };
297
+ }
298
+ if (id.length >= FULL_ID_LENGTH)
299
+ return { id };
300
+ const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
301
+ if (!res.json?.success)
302
+ return { error: describeFailure(res), code: EXIT.error };
303
+ const matches = (res.json.data ?? []).filter((s) => s.id.startsWith(id));
304
+ if (matches.length === 1)
305
+ return { id: matches[0].id };
306
+ if (matches.length === 0)
307
+ return { error: `no session starts with "${id}" (see \`agent ls\`)`, code: EXIT.error };
308
+ return { error: `"${id}" is ambiguous: ${matches.map((s) => s.id.slice(0, 13)).join(', ')}`, code: EXIT.error };
309
+ }
310
+ /** `agent ls` — every session the caller can see, self marked. */
311
+ export async function agentLs(deps) {
312
+ const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
313
+ if (!res.json?.success)
314
+ return fail(deps, describeFailure(res));
315
+ const sessions = res.json.data ?? [];
316
+ if (deps.json) {
317
+ emitJson(deps, sessions.map((s) => ({ ...s, self: isSelfSession(deps.ctx.selfId, s.id) })));
318
+ return EXIT.ok;
319
+ }
320
+ if (sessions.length === 0) {
321
+ deps.io.out(palette.muted('(no sessions)'));
322
+ return EXIT.ok;
323
+ }
324
+ const rows = sessions.map((s) => [
325
+ isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ',
326
+ s.id.slice(0, 8),
327
+ s.mode ?? '?',
328
+ s.status ?? '?',
329
+ s.name || s.workingDir || '',
330
+ ]);
331
+ deps.io.out(table([[' ', 'ID', 'MODE', 'STATUS', 'NAME'], ...rows], { gap: 2 }));
332
+ deps.io.out(palette.muted(`* = this session (${deps.ctx.selfId.slice(0, 8)}). status is a UI hint, never a sync signal.`));
333
+ return EXIT.ok;
334
+ }
335
+ /**
336
+ * Agent-scratch label for a case directory a spawn CREATES (the server applies it only
337
+ * when quick-start makes the directory). The Add Case UI offers a recursive delete for
338
+ * such directories, so this header must never ride any other request: mislabelling a
339
+ * real repo there is the one failure in this area that costs actual work.
340
+ */
341
+ export const AGENT_ORIGIN_HEADER = { 'X-Codeman-Agent-Origin': 'codeman-agent-cli' };
342
+ /**
343
+ * What the mode's TUI draws once its composer can take a prompt, from the CLI registry
344
+ * (`capabilities.composerReadyMark`); undefined means the mode has no readiness wait.
345
+ */
346
+ export function composerReadyMark(mode) {
347
+ return getCli(mode)?.capabilities.composerReadyMark;
348
+ }
349
+ /** `agent spawn` — quick-start with lineage, then the readiness ladder where the mode has one. */
350
+ export async function agentSpawn(deps, options) {
351
+ const body = {
352
+ caseName: options.caseName,
353
+ mode: options.mode,
354
+ parentSessionId: deps.ctx.selfId,
355
+ };
356
+ if (options.name)
357
+ body.sessionName = options.name;
358
+ const res = await deps.request(deps.ctx, {
359
+ method: 'POST',
360
+ path: '/api/v1/quick-start',
361
+ body,
362
+ headers: { ...AGENT_ORIGIN_HEADER },
363
+ });
364
+ const data = res.json?.data;
365
+ if (!res.json?.success || !data?.sessionId)
366
+ return fail(deps, describeFailure(res));
367
+ const sid = data.sessionId;
368
+ let ready;
369
+ let readinessError;
370
+ let dead = false;
371
+ const mark = composerReadyMark(options.mode);
372
+ if (options.ready && mark) {
373
+ const wait = await deps.request(deps.ctx, {
374
+ method: 'GET',
375
+ path: `/api/v1/sessions/${encodeURIComponent(sid)}/wait-output`,
376
+ query: { match: mark, from: 'buffer', timeout: options.timeoutMs },
377
+ timeoutMs: options.timeoutMs + 10_000,
378
+ });
379
+ // A failed readiness call (waiter cap, 400, network) is its own error, not "the
380
+ // composer never showed up": report the real reason instead of the trust-dialog hint.
381
+ if (!wait.json?.success)
382
+ readinessError = describeFailure(wait);
383
+ else {
384
+ const result = wait.json.data?.wait;
385
+ // A worker that died while we waited is exit 3 like every other wait, not a
386
+ // "composer not seen" timeout that sends the caller looking for a dialog.
387
+ dead = waitExitCode(result) === EXIT.dead;
388
+ ready = !dead && Boolean(result?.matched);
389
+ }
390
+ }
391
+ if (deps.json) {
392
+ emitJson(deps, { ...data, ready, readinessError });
393
+ }
394
+ else {
395
+ // Human lines go to stderr so `SID=$(codeman agent spawn …)` captures the id alone.
396
+ const say = (line) => deps.io.err(line);
397
+ say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode}, case ${data.caseName ?? options.caseName})`));
398
+ if (ready === true)
399
+ say(palette.muted(' composer up: the worker can take a prompt'));
400
+ if (dead)
401
+ say(palette.err(`${GLYPH.fail} the worker exited during the readiness wait`));
402
+ if (ready === false && !dead) {
403
+ say(palette.warn(`${GLYPH.warn} composer not seen within ${options.timeoutMs} ms — read \`agent read ${sid.slice(0, 8)} --tail 2000\` before sending (a startup dialog?)`));
404
+ }
405
+ if (readinessError)
406
+ say(palette.err(`${GLYPH.fail} readiness check failed: ${readinessError}`));
407
+ if (ready === undefined && !readinessError && options.ready) {
408
+ say(palette.muted(` ${options.mode} has no readiness mark; give it a moment, then use --match markers to synchronize`));
409
+ }
410
+ deps.io.out(sid);
411
+ }
412
+ if (readinessError)
413
+ return EXIT.error;
414
+ if (dead)
415
+ return EXIT.dead;
416
+ return ready === false ? EXIT.timeout : EXIT.ok;
417
+ }
418
+ /** `agent send` — printable text plus `\r`, exactly-once, optionally blocking on end of turn. */
419
+ export async function agentSend(deps, options) {
420
+ if (isSelfSession(deps.ctx.selfId, options.id)) {
421
+ return fail(deps, `refusing: ${options.id} is me — typing into my own composer is not a message`, EXIT.refused);
422
+ }
423
+ const refusal = inputRefusal(options.text);
424
+ if (refusal)
425
+ return fail(deps, refusal, EXIT.refused);
426
+ const target = await resolveSessionId(deps, options.id);
427
+ if ('error' in target)
428
+ return fail(deps, target.error, target.code);
429
+ const body = buildSendBody(options.text, {
430
+ enter: options.enter,
431
+ clientId: options.clientId ?? defaultClientId(deps.ctx.selfId),
432
+ seq: options.seq ?? (deps.now ?? Date.now)(),
433
+ wait: options.wait,
434
+ waitTimeout: options.wait !== undefined ? options.timeoutMs : undefined,
435
+ });
436
+ const res = await deps.request(deps.ctx, {
437
+ method: 'POST',
438
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/input`,
439
+ body,
440
+ timeoutMs: (options.timeoutMs ?? 60_000) + 10_000,
441
+ });
442
+ if (!res.json?.success)
443
+ return fail(deps, describeFailure(res));
444
+ const data = res.json.data;
445
+ if (deps.json)
446
+ emitJson(deps, data ?? {});
447
+ // Fire-and-forget to a remote session whose host is asleep (wake-on-LAN): the server
448
+ // holds the chunk and types it once the pane is back (`buffered`), or the chunk was
449
+ // over the wake buffer's cap and is gone (`dropped`). The seq is spent either way,
450
+ // so a retry needs a new one (the default, the clock, gives it that).
451
+ if (data?.dropped) {
452
+ if (!deps.json) {
453
+ deps.io.err(palette.err(`${GLYPH.fail} dropped: ${target.id}'s host is waking and its input buffer is full — nothing will be typed; send again once it is back`));
454
+ }
455
+ return EXIT.error;
456
+ }
457
+ // `delivered:false` without `duplicate` is the route's "the bytes went nowhere":
458
+ // the PTY exited or send-keys hit a dead pane. The field exists so a client does not
459
+ // say "wait longer" when the truth is "restart the worker" — so it is a failure here.
460
+ if (data?.delivered === false && !data.duplicate) {
461
+ if (!deps.json) {
462
+ deps.io.err(palette.err(`${GLYPH.fail} not delivered: ${target.id} has no live worker (pane exited) — restart it`));
463
+ }
464
+ return EXIT.dead;
465
+ }
466
+ if (!deps.json) {
467
+ const noEnter = options.enter ? '' : ' (no Enter)';
468
+ if (data?.duplicate) {
469
+ deps.io.out(palette.warn(`${GLYPH.warn} duplicate (clientId/seq already applied): nothing typed`));
470
+ }
471
+ else if (data?.buffered) {
472
+ deps.io.out(palette.ok(`${GLYPH.ok} buffered for ${target.id}${noEnter}: its host is asleep; Codeman is waking it and types this once the pane is back`));
473
+ }
474
+ else if (data?.delivered === true) {
475
+ deps.io.out(palette.ok(`${GLYPH.ok} delivered to ${target.id}${noEnter}`));
476
+ }
477
+ else {
478
+ // Fire-and-forget answers before the write, so there is no delivery report here.
479
+ deps.io.out(palette.ok(`${GLYPH.ok} accepted for ${target.id}${noEnter} (no delivery report without --wait)`));
480
+ }
481
+ if (data?.wait)
482
+ deps.io.out(describeWait(data.wait));
483
+ }
484
+ if (options.wait === undefined)
485
+ return EXIT.ok;
486
+ return waitExitCode(data?.wait);
487
+ }
488
+ function describeWait(wait) {
489
+ if (wait.signal === 'exit')
490
+ return palette.err(`${GLYPH.fail} the session exited`);
491
+ if (wait.ended) {
492
+ return palette.err(`${GLYPH.fail} the wait ended without an answer: the session went away (dead worker, deleted, or nothing was written)`);
493
+ }
494
+ // A timeout is a 200 with `timedOut`, an answer rather than a failure: exit 2 says it,
495
+ // so the line stays neutral instead of looking like an error to whoever reads the log.
496
+ if (wait.timedOut)
497
+ return palette.muted(`timed out after ${wait.timeoutMs ?? '?'} ms (exit 2)`);
498
+ if (wait.matched !== undefined) {
499
+ return wait.matched
500
+ ? palette.ok(`${GLYPH.ok} matched "${wait.match}"${wait.snippet ? `: ${wait.snippet}` : ''}`)
501
+ : palette.muted('not matched (exit 2)');
502
+ }
503
+ return palette.ok(`${GLYPH.ok} signal: ${wait.signal}${wait.immediate ? ' (immediate: current state, not a transition)' : ''}`);
504
+ }
505
+ /** `agent wait` — a signal (`--until`) or a literal output marker (`--match`). */
506
+ export async function agentWait(deps, options) {
507
+ if (options.until && options.match)
508
+ return fail(deps, 'use either --until <signals> or --match <marker>, not both', EXIT.refused);
509
+ const target = await resolveSessionId(deps, options.id);
510
+ if ('error' in target)
511
+ return fail(deps, target.error, target.code);
512
+ const sid = encodeURIComponent(target.id);
513
+ const res = options.match
514
+ ? await deps.request(deps.ctx, {
515
+ method: 'GET',
516
+ path: `/api/v1/sessions/${sid}/wait-output`,
517
+ query: {
518
+ match: options.match,
519
+ from: options.from ?? 'buffer',
520
+ nocase: options.nocase ? 1 : undefined,
521
+ timeout: options.timeoutMs,
522
+ },
523
+ timeoutMs: options.timeoutMs + 10_000,
524
+ })
525
+ : await deps.request(deps.ctx, {
526
+ method: 'GET',
527
+ path: `/api/v1/sessions/${sid}/wait`,
528
+ query: { until: options.until, fresh: options.fresh ? 1 : undefined, timeout: options.timeoutMs },
529
+ timeoutMs: options.timeoutMs + 10_000,
530
+ });
531
+ // A 400 here is the server saying "this mode has no such signal" (until=stop on an
532
+ // external CLI). Passed through, never papered over: the marker path is the answer.
533
+ if (!res.json?.success)
534
+ return fail(deps, describeFailure(res));
535
+ const data = res.json.data;
536
+ if (deps.json) {
537
+ emitJson(deps, data ?? {});
538
+ }
539
+ else if (data?.wait) {
540
+ deps.io.out(describeWait(data.wait));
541
+ if (data.limitPaused)
542
+ deps.io.out(palette.warn(`${GLYPH.warn} session is paused on a usage limit; a timeout is expected`));
543
+ }
544
+ return waitExitCode(data?.wait);
545
+ }
546
+ /** `agent read` — the last answer (the route picks the transcript reader or the pane segmenter) or a terminal tail. */
547
+ export async function agentRead(deps, options) {
548
+ const target = await resolveSessionId(deps, options.id);
549
+ if ('error' in target)
550
+ return fail(deps, target.error, target.code);
551
+ const sid = encodeURIComponent(target.id);
552
+ if (options.tail !== undefined) {
553
+ const res = await deps.request(deps.ctx, {
554
+ method: 'GET',
555
+ path: `/api/v1/sessions/${sid}/terminal`,
556
+ query: { tail: options.tail },
557
+ });
558
+ if (!res.json?.success)
559
+ return fail(deps, describeFailure(res));
560
+ const buffer = res.json.data?.terminalBuffer ?? '';
561
+ if (deps.json)
562
+ emitJson(deps, res.json.data);
563
+ else
564
+ deps.io.out(stripAnsi(buffer));
565
+ return EXIT.ok;
566
+ }
567
+ const res = await deps.request(deps.ctx, {
568
+ method: 'GET',
569
+ path: `/api/v1/sessions/${sid}/last-response`,
570
+ query: { context: options.full ? 'full' : undefined },
571
+ });
572
+ if (!res.json?.success)
573
+ return fail(deps, describeFailure(res));
574
+ const data = res.json.data;
575
+ if (deps.json) {
576
+ emitJson(deps, data ?? {});
577
+ return EXIT.ok;
578
+ }
579
+ if (options.full && data?.messages) {
580
+ for (const m of data.messages)
581
+ deps.io.out(`${palette.emph(m.role)}: ${m.text}`);
582
+ return EXIT.ok;
583
+ }
584
+ const text = data?.text ?? '';
585
+ if (!text) {
586
+ deps.io.err(palette.muted('(empty: nothing answered yet, or nothing the server could segment as an answer; try --tail 3000)'));
587
+ return EXIT.ok;
588
+ }
589
+ deps.io.out(text);
590
+ return EXIT.ok;
591
+ }
592
+ /** `agent interrupt` — a bare ESC keypress, no Enter, conversation intact. */
593
+ export async function agentInterrupt(deps, options) {
594
+ if (isSelfSession(deps.ctx.selfId, options.id))
595
+ return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
596
+ const target = await resolveSessionId(deps, options.id);
597
+ if ('error' in target)
598
+ return fail(deps, target.error, target.code);
599
+ const body = buildInterruptBody(defaultClientId(deps.ctx.selfId, 'interrupt'), (deps.now ?? Date.now)());
600
+ const res = await deps.request(deps.ctx, {
601
+ method: 'POST',
602
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/input`,
603
+ body,
604
+ });
605
+ if (!res.json?.success)
606
+ return fail(deps, describeFailure(res));
607
+ if (deps.json)
608
+ emitJson(deps, res.json.data ?? {});
609
+ else
610
+ deps.io.out(palette.ok(`${GLYPH.ok} ESC sent to ${target.id} — one Esc does not always land; read the tail before the next prompt`));
611
+ return EXIT.ok;
612
+ }
613
+ /** `agent rm` — delete a session that is provably not this one. */
614
+ export async function agentRm(deps, options) {
615
+ const refusal = deleteRefusal(deps.ctx.selfId, options.id);
616
+ if (refusal)
617
+ return fail(deps, refusal, EXIT.refused);
618
+ const target = await resolveSessionId(deps, options.id);
619
+ if ('error' in target)
620
+ return fail(deps, target.error, target.code);
621
+ // The guard again on the RESOLVED id: a prefix that is not me can still resolve
622
+ // to me only if the list is lying, but a delete is the one call worth the paranoia.
623
+ const resolvedRefusal = deleteRefusal(deps.ctx.selfId, target.id);
624
+ if (resolvedRefusal)
625
+ return fail(deps, resolvedRefusal, EXIT.refused);
626
+ const res = await deps.request(deps.ctx, {
627
+ method: 'DELETE',
628
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}`,
629
+ });
630
+ if (!res.json?.success)
631
+ return fail(deps, describeFailure(res));
632
+ if (deps.json)
633
+ emitJson(deps, res.json.data ?? {});
634
+ else
635
+ deps.io.out(palette.ok(`${GLYPH.ok} deleted ${target.id} (its case directory stays on disk)`));
636
+ return EXIT.ok;
637
+ }
638
+ // ─────────────────────────────────────────────────────────────────────────────
639
+ // Commander wiring
640
+ // ─────────────────────────────────────────────────────────────────────────────
641
+ const DEFAULT_WAIT_MS = 60_000;
642
+ /** Build deps from the live environment; the guard's message is the only thing a non-session caller sees. */
643
+ function liveDeps(json) {
644
+ try {
645
+ return {
646
+ ctx: resolveAgentContext(),
647
+ request: httpRequest,
648
+ json,
649
+ io: { out: (line) => console.log(line), err: (line) => console.error(line) },
650
+ };
651
+ }
652
+ catch (err) {
653
+ if (err instanceof AgentGuardError) {
654
+ console.error(palette.err(`${GLYPH.fail} ${err.message}`));
655
+ return undefined;
656
+ }
657
+ throw err;
658
+ }
659
+ }
660
+ /** Run a verb with the live transport and turn its exit code into the process exit. */
661
+ async function run(json, verb) {
662
+ const deps = liveDeps(json);
663
+ if (!deps) {
664
+ process.exitCode = EXIT.refused;
665
+ return;
666
+ }
667
+ try {
668
+ process.exitCode = await verb(deps);
669
+ }
670
+ catch (err) {
671
+ console.error(palette.err(`${GLYPH.fail} ${getErrorMessage(err)}`));
672
+ process.exitCode = EXIT.error;
673
+ }
674
+ }
675
+ /** Register `codeman agent …` on the program. */
676
+ export function registerAgentCommands(program) {
677
+ const agent = program
678
+ .command('agent')
679
+ .description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm');
680
+ agent
681
+ .command('ls')
682
+ .alias('list')
683
+ .description('List sessions; * marks this one')
684
+ .option('--json', 'Machine-readable output')
685
+ .action((options) => run(Boolean(options.json), agentLs));
686
+ agent
687
+ .command('spawn <case>')
688
+ .description('Start a worker session in a case (created if missing) and wait for its composer where the mode draws one')
689
+ .option('-m, --mode <mode>', 'Run mode id, as the Run menu names it', 'claude')
690
+ .option('-n, --name <name>', 'Session name shown in the UI')
691
+ .option('--no-ready', 'Return as soon as the session exists, without the readiness wait')
692
+ .option('-t, --timeout <ms>', 'Readiness budget in ms', String(DEFAULT_WAIT_MS))
693
+ .option('--json', 'Machine-readable output')
694
+ .action((caseName, options) => run(Boolean(options.json), (deps) => agentSpawn(deps, {
695
+ caseName,
696
+ mode: options.mode,
697
+ name: options.name,
698
+ ready: options.ready,
699
+ timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
700
+ })));
701
+ agent
702
+ .command('send <id> <text...>')
703
+ .description('Type a prompt into another session and press Enter (ONE quoted argument, printable text only; a prompt that starts with "-" goes after --: send <id> -- "- fix the bug")')
704
+ .option('-w, --wait', 'Block until end of turn (the default signal set; see --until)')
705
+ .option('-u, --until <signals>', 'Signals to wait for, comma list such as stop,exit (implies --wait)')
706
+ .option('-t, --timeout <ms>', 'Wait budget in ms (with --wait)', String(DEFAULT_WAIT_MS))
707
+ .option('--no-enter', 'Type the text without submitting it')
708
+ .option('--client-id <id>', 'Exactly-once tag (default: one per calling session)')
709
+ .option('--seq <n>', 'Sequence number for the tag (default: the current epoch ms). Must stay monotonic per client id: a reused or lower value is a silent duplicate, nothing is typed')
710
+ .option('--json', 'Machine-readable output')
711
+ .action((id, words, options) => run(Boolean(options.json), (deps) => {
712
+ const prompt = sendPromptFromArgs(words);
713
+ if ('error' in prompt)
714
+ return Promise.resolve(fail(deps, prompt.error, EXIT.refused));
715
+ return agentSend(deps, {
716
+ id,
717
+ text: prompt.text,
718
+ enter: options.enter,
719
+ wait: options.until ?? (options.wait ? true : undefined),
720
+ timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
721
+ clientId: options.clientId,
722
+ seq: options.seq === undefined ? undefined : parsePositiveInt(options.seq, 1, '--seq'),
723
+ });
724
+ }));
725
+ agent
726
+ .command('wait <id>')
727
+ .description('Block until a signal (--until) or an output marker (--match) — timeout exits 2, a dead worker exits 3')
728
+ .option('-u, --until <signals>', 'Comma list: stop,idle,exit,working,blocked (stop/blocked need hook signals for the session; where there are none the server answers 400, passed through)')
729
+ .option('-m, --match <marker>', 'Literal substring to wait for in the output (ANSI-stripped, no regex). The echo of your own prompt is output too, so never put the marker verbatim in the prompt: ask for it in halves ("print WORKDONE followed by _4711") and wait on the joined form (WORKDONE_4711)')
730
+ .option('--from <where>', 'buffer (scan existing output first, the default) or now', 'buffer')
731
+ .option('--nocase', 'Case-insensitive --match')
732
+ .option('--fresh', 'Require an actual transition (--until only)')
733
+ .option('-t, --timeout <ms>', 'Budget in ms', String(DEFAULT_WAIT_MS))
734
+ .option('--json', 'Machine-readable output')
735
+ .action((id, options) => run(Boolean(options.json), (deps) => agentWait(deps, {
736
+ id,
737
+ until: options.until,
738
+ match: options.match,
739
+ from: options.from === 'now' ? 'now' : 'buffer',
740
+ nocase: options.nocase,
741
+ fresh: options.fresh,
742
+ timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
743
+ })));
744
+ agent
745
+ .command('read <id>')
746
+ .description("Print a session's last answer (as the server reads it for that mode) or, with --tail, its terminal")
747
+ .option('--tail <bytes>', 'Raw terminal tail in bytes, ANSI stripped (works in every mode)')
748
+ .option('--full', 'The whole conversation instead of the last assistant message')
749
+ .option('--json', 'Machine-readable output')
750
+ .action((id, options) => run(Boolean(options.json), (deps) => agentRead(deps, {
751
+ id,
752
+ tail: options.tail === undefined ? undefined : parsePositiveInt(options.tail, 3000, '--tail'),
753
+ full: options.full,
754
+ })));
755
+ agent
756
+ .command('interrupt <id>')
757
+ .description('Send a bare ESC to stop the current turn (the conversation survives; deleting would not)')
758
+ .option('--json', 'Machine-readable output')
759
+ .action((id, options) => run(Boolean(options.json), (deps) => agentInterrupt(deps, { id })));
760
+ agent
761
+ .command('rm <id>')
762
+ .description('Delete any session except this one (refuses your own id)')
763
+ .option('--json', 'Machine-readable output')
764
+ .action((id, options) => run(Boolean(options.json), (deps) => agentRm(deps, { id })));
765
+ return agent;
766
+ }
767
+ //# sourceMappingURL=cli-agent.js.map