aicodeman 0.2.9 → 0.3.1

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 (374) hide show
  1. package/README.md +118 -4
  2. package/dist/ai-idle-checker.d.ts.map +1 -1
  3. package/dist/ai-idle-checker.js +3 -2
  4. package/dist/ai-idle-checker.js.map +1 -1
  5. package/dist/ai-plan-checker.d.ts.map +1 -1
  6. package/dist/ai-plan-checker.js +3 -2
  7. package/dist/ai-plan-checker.js.map +1 -1
  8. package/dist/bash-tool-parser.d.ts +2 -3
  9. package/dist/bash-tool-parser.d.ts.map +1 -1
  10. package/dist/bash-tool-parser.js +14 -31
  11. package/dist/bash-tool-parser.js.map +1 -1
  12. package/dist/config/ai-defaults.d.ts +16 -0
  13. package/dist/config/ai-defaults.d.ts.map +1 -0
  14. package/dist/config/ai-defaults.js +16 -0
  15. package/dist/config/ai-defaults.js.map +1 -0
  16. package/dist/config/auth-config.d.ts +19 -0
  17. package/dist/config/auth-config.d.ts.map +1 -0
  18. package/dist/config/auth-config.js +28 -0
  19. package/dist/config/auth-config.js.map +1 -0
  20. package/dist/config/exec-timeout.d.ts +10 -0
  21. package/dist/config/exec-timeout.d.ts.map +1 -0
  22. package/dist/config/exec-timeout.js +10 -0
  23. package/dist/config/exec-timeout.js.map +1 -0
  24. package/dist/config/map-limits.d.ts +4 -0
  25. package/dist/config/map-limits.d.ts.map +1 -1
  26. package/dist/config/map-limits.js +7 -0
  27. package/dist/config/map-limits.js.map +1 -1
  28. package/dist/config/server-timing.d.ts +42 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +57 -0
  31. package/dist/config/server-timing.js.map +1 -0
  32. package/dist/config/team-config.d.ts +16 -0
  33. package/dist/config/team-config.d.ts.map +1 -0
  34. package/dist/config/team-config.js +16 -0
  35. package/dist/config/team-config.js.map +1 -0
  36. package/dist/config/terminal-limits.d.ts +18 -0
  37. package/dist/config/terminal-limits.d.ts.map +1 -0
  38. package/dist/config/terminal-limits.js +18 -0
  39. package/dist/config/terminal-limits.js.map +1 -0
  40. package/dist/config/tunnel-config.d.ts +27 -0
  41. package/dist/config/tunnel-config.d.ts.map +1 -0
  42. package/dist/config/tunnel-config.js +36 -0
  43. package/dist/config/tunnel-config.js.map +1 -0
  44. package/dist/hooks-config.d.ts +21 -6
  45. package/dist/hooks-config.d.ts.map +1 -1
  46. package/dist/hooks-config.js +28 -12
  47. package/dist/hooks-config.js.map +1 -1
  48. package/dist/image-watcher.d.ts +4 -4
  49. package/dist/image-watcher.d.ts.map +1 -1
  50. package/dist/image-watcher.js +17 -30
  51. package/dist/image-watcher.js.map +1 -1
  52. package/dist/index.js +1 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/plan-orchestrator.d.ts +2 -24
  55. package/dist/plan-orchestrator.d.ts.map +1 -1
  56. package/dist/plan-orchestrator.js.map +1 -1
  57. package/dist/prompts/planner.d.ts +7 -8
  58. package/dist/prompts/planner.d.ts.map +1 -1
  59. package/dist/prompts/planner.js +7 -8
  60. package/dist/prompts/planner.js.map +1 -1
  61. package/dist/prompts/research-agent.d.ts +6 -4
  62. package/dist/prompts/research-agent.d.ts.map +1 -1
  63. package/dist/prompts/research-agent.js +6 -4
  64. package/dist/prompts/research-agent.js.map +1 -1
  65. package/dist/push-store.d.ts +1 -1
  66. package/dist/push-store.d.ts.map +1 -1
  67. package/dist/push-store.js +4 -12
  68. package/dist/push-store.js.map +1 -1
  69. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  70. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  71. package/dist/ralph-fix-plan-watcher.js +326 -0
  72. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  73. package/dist/ralph-loop.d.ts +14 -4
  74. package/dist/ralph-loop.d.ts.map +1 -1
  75. package/dist/ralph-loop.js +14 -4
  76. package/dist/ralph-loop.js.map +1 -1
  77. package/dist/ralph-plan-tracker.d.ts +201 -0
  78. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  79. package/dist/ralph-plan-tracker.js +325 -0
  80. package/dist/ralph-plan-tracker.js.map +1 -0
  81. package/dist/ralph-stall-detector.d.ts +84 -0
  82. package/dist/ralph-stall-detector.d.ts.map +1 -0
  83. package/dist/ralph-stall-detector.js +139 -0
  84. package/dist/ralph-stall-detector.js.map +1 -0
  85. package/dist/ralph-status-parser.d.ts +141 -0
  86. package/dist/ralph-status-parser.d.ts.map +1 -0
  87. package/dist/ralph-status-parser.js +478 -0
  88. package/dist/ralph-status-parser.js.map +1 -0
  89. package/dist/ralph-tracker.d.ts +218 -692
  90. package/dist/ralph-tracker.d.ts.map +1 -1
  91. package/dist/ralph-tracker.js +389 -1723
  92. package/dist/ralph-tracker.js.map +1 -1
  93. package/dist/respawn-adaptive-timing.d.ts +61 -0
  94. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  95. package/dist/respawn-adaptive-timing.js +105 -0
  96. package/dist/respawn-adaptive-timing.js.map +1 -0
  97. package/dist/respawn-controller.d.ts +35 -115
  98. package/dist/respawn-controller.d.ts.map +1 -1
  99. package/dist/respawn-controller.js +167 -607
  100. package/dist/respawn-controller.js.map +1 -1
  101. package/dist/respawn-health.d.ts +54 -0
  102. package/dist/respawn-health.d.ts.map +1 -0
  103. package/dist/respawn-health.js +183 -0
  104. package/dist/respawn-health.js.map +1 -0
  105. package/dist/respawn-metrics.d.ts +81 -0
  106. package/dist/respawn-metrics.d.ts.map +1 -0
  107. package/dist/respawn-metrics.js +198 -0
  108. package/dist/respawn-metrics.js.map +1 -0
  109. package/dist/respawn-patterns.d.ts +45 -0
  110. package/dist/respawn-patterns.d.ts.map +1 -0
  111. package/dist/respawn-patterns.js +125 -0
  112. package/dist/respawn-patterns.js.map +1 -0
  113. package/dist/session-auto-ops.d.ts +89 -0
  114. package/dist/session-auto-ops.d.ts.map +1 -0
  115. package/dist/session-auto-ops.js +224 -0
  116. package/dist/session-auto-ops.js.map +1 -0
  117. package/dist/session-cli-builder.d.ts +62 -0
  118. package/dist/session-cli-builder.d.ts.map +1 -0
  119. package/dist/session-cli-builder.js +121 -0
  120. package/dist/session-cli-builder.js.map +1 -0
  121. package/dist/session-manager.d.ts +17 -5
  122. package/dist/session-manager.d.ts.map +1 -1
  123. package/dist/session-manager.js +17 -5
  124. package/dist/session-manager.js.map +1 -1
  125. package/dist/session-task-cache.d.ts +52 -0
  126. package/dist/session-task-cache.d.ts.map +1 -0
  127. package/dist/session-task-cache.js +90 -0
  128. package/dist/session-task-cache.js.map +1 -0
  129. package/dist/session.d.ts +23 -41
  130. package/dist/session.d.ts.map +1 -1
  131. package/dist/session.js +79 -317
  132. package/dist/session.js.map +1 -1
  133. package/dist/state-store.d.ts +19 -9
  134. package/dist/state-store.d.ts.map +1 -1
  135. package/dist/state-store.js +29 -30
  136. package/dist/state-store.js.map +1 -1
  137. package/dist/subagent-watcher.d.ts +26 -7
  138. package/dist/subagent-watcher.d.ts.map +1 -1
  139. package/dist/subagent-watcher.js +47 -64
  140. package/dist/subagent-watcher.js.map +1 -1
  141. package/dist/team-watcher.d.ts.map +1 -1
  142. package/dist/team-watcher.js +2 -5
  143. package/dist/team-watcher.js.map +1 -1
  144. package/dist/tmux-manager.d.ts.map +1 -1
  145. package/dist/tmux-manager.js +1 -2
  146. package/dist/tmux-manager.js.map +1 -1
  147. package/dist/tunnel-manager.d.ts +26 -0
  148. package/dist/tunnel-manager.d.ts.map +1 -1
  149. package/dist/tunnel-manager.js +126 -7
  150. package/dist/tunnel-manager.js.map +1 -1
  151. package/dist/types/api.d.ts +108 -0
  152. package/dist/types/api.d.ts.map +1 -0
  153. package/dist/types/api.js +98 -0
  154. package/dist/types/api.js.map +1 -0
  155. package/dist/types/app-state.d.ts +117 -0
  156. package/dist/types/app-state.d.ts.map +1 -0
  157. package/dist/types/app-state.js +76 -0
  158. package/dist/types/app-state.js.map +1 -0
  159. package/dist/types/common.d.ts +79 -0
  160. package/dist/types/common.d.ts.map +1 -0
  161. package/dist/types/common.js +17 -0
  162. package/dist/types/common.js.map +1 -0
  163. package/dist/types/index.d.ts +66 -0
  164. package/dist/types/index.d.ts.map +1 -0
  165. package/dist/types/index.js +66 -0
  166. package/dist/types/index.js.map +1 -0
  167. package/dist/types/lifecycle.d.ts +28 -0
  168. package/dist/types/lifecycle.d.ts.map +1 -0
  169. package/dist/types/lifecycle.js +16 -0
  170. package/dist/types/lifecycle.js.map +1 -0
  171. package/dist/types/plan.d.ts +45 -0
  172. package/dist/types/plan.d.ts.map +1 -0
  173. package/dist/types/plan.js +18 -0
  174. package/dist/types/plan.js.map +1 -0
  175. package/dist/types/push.d.ts +36 -0
  176. package/dist/types/push.d.ts.map +1 -0
  177. package/dist/types/push.js +18 -0
  178. package/dist/types/push.js.map +1 -0
  179. package/dist/types/ralph.d.ts +262 -0
  180. package/dist/types/ralph.d.ts.map +1 -0
  181. package/dist/types/ralph.js +70 -0
  182. package/dist/types/ralph.js.map +1 -0
  183. package/dist/types/respawn.d.ts +271 -0
  184. package/dist/types/respawn.d.ts.map +1 -0
  185. package/dist/types/respawn.js +26 -0
  186. package/dist/types/respawn.js.map +1 -0
  187. package/dist/types/run-summary.d.ts +96 -0
  188. package/dist/types/run-summary.d.ts.map +1 -0
  189. package/dist/types/run-summary.js +37 -0
  190. package/dist/types/run-summary.js.map +1 -0
  191. package/dist/types/session.d.ts +152 -0
  192. package/dist/types/session.d.ts.map +1 -0
  193. package/dist/types/session.js +27 -0
  194. package/dist/types/session.js.map +1 -0
  195. package/dist/types/task.d.ts +72 -0
  196. package/dist/types/task.d.ts.map +1 -0
  197. package/dist/types/task.js +19 -0
  198. package/dist/types/task.js.map +1 -0
  199. package/dist/types/teams.d.ts +73 -0
  200. package/dist/types/teams.d.ts.map +1 -0
  201. package/dist/types/teams.js +23 -0
  202. package/dist/types/teams.js.map +1 -0
  203. package/dist/types/tools.d.ts +61 -0
  204. package/dist/types/tools.d.ts.map +1 -0
  205. package/dist/types/tools.js +20 -0
  206. package/dist/types/tools.js.map +1 -0
  207. package/dist/types.d.ts +8 -1134
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/types.js +8 -210
  210. package/dist/types.js.map +1 -1
  211. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  212. package/dist/utils/claude-cli-resolver.js +1 -2
  213. package/dist/utils/claude-cli-resolver.js.map +1 -1
  214. package/dist/utils/debouncer.d.ts +111 -0
  215. package/dist/utils/debouncer.d.ts.map +1 -0
  216. package/dist/utils/debouncer.js +162 -0
  217. package/dist/utils/debouncer.js.map +1 -0
  218. package/dist/utils/index.d.ts +3 -2
  219. package/dist/utils/index.d.ts.map +1 -1
  220. package/dist/utils/index.js +3 -2
  221. package/dist/utils/index.js.map +1 -1
  222. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  223. package/dist/utils/opencode-cli-resolver.js +1 -2
  224. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  225. package/dist/utils/string-similarity.d.ts +0 -57
  226. package/dist/utils/string-similarity.d.ts.map +1 -1
  227. package/dist/utils/string-similarity.js +3 -18
  228. package/dist/utils/string-similarity.js.map +1 -1
  229. package/dist/web/middleware/auth.d.ts +31 -0
  230. package/dist/web/middleware/auth.d.ts.map +1 -0
  231. package/dist/web/middleware/auth.js +154 -0
  232. package/dist/web/middleware/auth.js.map +1 -0
  233. package/dist/web/ports/auth-port.d.ts +18 -0
  234. package/dist/web/ports/auth-port.d.ts.map +1 -0
  235. package/dist/web/ports/auth-port.js +6 -0
  236. package/dist/web/ports/auth-port.js.map +1 -0
  237. package/dist/web/ports/config-port.d.ts +28 -0
  238. package/dist/web/ports/config-port.d.ts.map +1 -0
  239. package/dist/web/ports/config-port.js +6 -0
  240. package/dist/web/ports/config-port.js.map +1 -0
  241. package/dist/web/ports/event-port.d.ts +13 -0
  242. package/dist/web/ports/event-port.d.ts.map +1 -0
  243. package/dist/web/ports/event-port.js +6 -0
  244. package/dist/web/ports/event-port.js.map +1 -0
  245. package/dist/web/ports/index.d.ts +14 -0
  246. package/dist/web/ports/index.d.ts.map +1 -0
  247. package/dist/web/ports/index.js +9 -0
  248. package/dist/web/ports/index.js.map +1 -0
  249. package/dist/web/ports/infra-port.d.ts +36 -0
  250. package/dist/web/ports/infra-port.d.ts.map +1 -0
  251. package/dist/web/ports/infra-port.js +6 -0
  252. package/dist/web/ports/infra-port.js.map +1 -0
  253. package/dist/web/ports/respawn-port.d.ts +20 -0
  254. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  255. package/dist/web/ports/respawn-port.js +6 -0
  256. package/dist/web/ports/respawn-port.js.map +1 -0
  257. package/dist/web/ports/session-port.d.ts +15 -0
  258. package/dist/web/ports/session-port.d.ts.map +1 -0
  259. package/dist/web/ports/session-port.js +6 -0
  260. package/dist/web/ports/session-port.js.map +1 -0
  261. package/dist/web/public/api-client.js +82 -0
  262. package/dist/web/public/api-client.js.br +0 -0
  263. package/dist/web/public/api-client.js.gz +0 -0
  264. package/dist/web/public/app.js +117 -201
  265. package/dist/web/public/app.js.br +0 -0
  266. package/dist/web/public/app.js.gz +0 -0
  267. package/dist/web/public/constants.js +365 -0
  268. package/dist/web/public/constants.js.br +0 -0
  269. package/dist/web/public/constants.js.gz +0 -0
  270. package/dist/web/public/index.html +15 -3
  271. package/dist/web/public/index.html.br +0 -0
  272. package/dist/web/public/index.html.gz +0 -0
  273. package/dist/web/public/keyboard-accessory.js +302 -0
  274. package/dist/web/public/keyboard-accessory.js.br +0 -0
  275. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  276. package/dist/web/public/mobile-handlers.js +491 -0
  277. package/dist/web/public/mobile-handlers.js.br +0 -0
  278. package/dist/web/public/mobile-handlers.js.gz +0 -0
  279. package/dist/web/public/mobile.css.gz +0 -0
  280. package/dist/web/public/notification-manager.js +472 -0
  281. package/dist/web/public/notification-manager.js.br +0 -0
  282. package/dist/web/public/notification-manager.js.gz +0 -0
  283. package/dist/web/public/ralph-wizard.js +33 -9
  284. package/dist/web/public/ralph-wizard.js.br +0 -0
  285. package/dist/web/public/ralph-wizard.js.gz +0 -0
  286. package/dist/web/public/styles.css.gz +0 -0
  287. package/dist/web/public/subagent-windows.js +1149 -0
  288. package/dist/web/public/subagent-windows.js.br +0 -0
  289. package/dist/web/public/subagent-windows.js.gz +0 -0
  290. package/dist/web/public/sw.js +15 -0
  291. package/dist/web/public/sw.js.br +0 -0
  292. package/dist/web/public/sw.js.gz +0 -0
  293. package/dist/web/public/upload.html.gz +0 -0
  294. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  295. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  296. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  297. package/dist/web/public/vendor/xterm-zerolag-input.js +4 -0
  298. package/dist/web/public/vendor/xterm-zerolag-input.js.br +0 -0
  299. package/dist/web/public/vendor/xterm-zerolag-input.js.gz +0 -0
  300. package/dist/web/public/vendor/xterm.css.gz +0 -0
  301. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  302. package/dist/web/public/voice-input.js +882 -0
  303. package/dist/web/public/voice-input.js.br +0 -0
  304. package/dist/web/public/voice-input.js.gz +0 -0
  305. package/dist/web/route-helpers.d.ts +38 -0
  306. package/dist/web/route-helpers.d.ts.map +1 -0
  307. package/dist/web/route-helpers.js +144 -0
  308. package/dist/web/route-helpers.js.map +1 -0
  309. package/dist/web/routes/case-routes.d.ts +9 -0
  310. package/dist/web/routes/case-routes.d.ts.map +1 -0
  311. package/dist/web/routes/case-routes.js +426 -0
  312. package/dist/web/routes/case-routes.js.map +1 -0
  313. package/dist/web/routes/file-routes.d.ts +8 -0
  314. package/dist/web/routes/file-routes.d.ts.map +1 -0
  315. package/dist/web/routes/file-routes.js +337 -0
  316. package/dist/web/routes/file-routes.js.map +1 -0
  317. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  318. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  319. package/dist/web/routes/hook-event-routes.js +57 -0
  320. package/dist/web/routes/hook-event-routes.js.map +1 -0
  321. package/dist/web/routes/index.d.ts +16 -0
  322. package/dist/web/routes/index.d.ts.map +1 -0
  323. package/dist/web/routes/index.js +16 -0
  324. package/dist/web/routes/index.js.map +1 -0
  325. package/dist/web/routes/mux-routes.d.ts +8 -0
  326. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  327. package/dist/web/routes/mux-routes.js +32 -0
  328. package/dist/web/routes/mux-routes.js.map +1 -0
  329. package/dist/web/routes/plan-routes.d.ts +9 -0
  330. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  331. package/dist/web/routes/plan-routes.js +385 -0
  332. package/dist/web/routes/plan-routes.js.map +1 -0
  333. package/dist/web/routes/push-routes.d.ts +8 -0
  334. package/dist/web/routes/push-routes.d.ts.map +1 -0
  335. package/dist/web/routes/push-routes.js +49 -0
  336. package/dist/web/routes/push-routes.js.map +1 -0
  337. package/dist/web/routes/ralph-routes.d.ts +9 -0
  338. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  339. package/dist/web/routes/ralph-routes.js +485 -0
  340. package/dist/web/routes/ralph-routes.js.map +1 -0
  341. package/dist/web/routes/respawn-routes.d.ts +8 -0
  342. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  343. package/dist/web/routes/respawn-routes.js +270 -0
  344. package/dist/web/routes/respawn-routes.js.map +1 -0
  345. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  346. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  347. package/dist/web/routes/scheduled-routes.js +51 -0
  348. package/dist/web/routes/scheduled-routes.js.map +1 -0
  349. package/dist/web/routes/session-routes.d.ts +9 -0
  350. package/dist/web/routes/session-routes.d.ts.map +1 -0
  351. package/dist/web/routes/session-routes.js +751 -0
  352. package/dist/web/routes/session-routes.js.map +1 -0
  353. package/dist/web/routes/system-routes.d.ts +9 -0
  354. package/dist/web/routes/system-routes.d.ts.map +1 -0
  355. package/dist/web/routes/system-routes.js +699 -0
  356. package/dist/web/routes/system-routes.js.map +1 -0
  357. package/dist/web/routes/team-routes.d.ts +8 -0
  358. package/dist/web/routes/team-routes.d.ts.map +1 -0
  359. package/dist/web/routes/team-routes.js +14 -0
  360. package/dist/web/routes/team-routes.js.map +1 -0
  361. package/dist/web/schemas.d.ts +43 -3
  362. package/dist/web/schemas.d.ts.map +1 -1
  363. package/dist/web/schemas.js +6 -2
  364. package/dist/web/schemas.js.map +1 -1
  365. package/dist/web/server.d.ts +35 -15
  366. package/dist/web/server.d.ts.map +1 -1
  367. package/dist/web/server.js +563 -3971
  368. package/dist/web/server.js.map +1 -1
  369. package/dist/web/sse-events.d.ts +361 -0
  370. package/dist/web/sse-events.d.ts.map +1 -0
  371. package/dist/web/sse-events.js +396 -0
  372. package/dist/web/sse-events.js.map +1 -0
  373. package/package.json +2 -1
  374. package/scripts/postinstall.js +58 -0
@@ -0,0 +1,699 @@
1
+ /**
2
+ * @fileoverview System, config, settings, subagent, and debug routes.
3
+ * Covers status, stats, config CRUD, settings, subagent monitoring,
4
+ * debug/memory, lifecycle logs, screenshots, and various persistence endpoints.
5
+ */
6
+ import { join, dirname } from 'node:path';
7
+ import { existsSync, mkdirSync, readdirSync } from 'node:fs';
8
+ import fs from 'node:fs/promises';
9
+ import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os';
10
+ import { execSync } from 'node:child_process';
11
+ import { randomBytes } from 'node:crypto';
12
+ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
13
+ import { ConfigUpdateSchema, SettingsUpdateSchema, ModelConfigUpdateSchema, CpuLimitSchema, SubagentWindowStatesSchema, SubagentParentMapSchema, RevokeSessionSchema, } from '../schemas.js';
14
+ import { subagentWatcher } from '../../subagent-watcher.js';
15
+ import { imageWatcher } from '../../image-watcher.js';
16
+ import { getLifecycleLog } from '../../session-lifecycle-log.js';
17
+ import { findSessionOrFail, formatUptime, SETTINGS_PATH } from '../route-helpers.js';
18
+ import { SseEvent } from '../sse-events.js';
19
+ import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
20
+ import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
21
+ import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
22
+ // Maximum screenshot upload size (10MB)
23
+ const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
24
+ // Screenshots directory
25
+ const SCREENSHOTS_DIR = join(homedir(), '.codeman', 'screenshots');
26
+ /** Cached CPU count — doesn't change at runtime */
27
+ const CPU_COUNT = cpus().length;
28
+ /** Get system CPU and memory usage */
29
+ function getSystemStats() {
30
+ try {
31
+ const totalMem = totalmem();
32
+ // macOS: os.freemem() only returns truly free pages, not cached/purgeable memory.
33
+ // Use vm_stat to get accurate used memory (wired + active + compressed).
34
+ let usedMem;
35
+ if (process.platform === 'darwin') {
36
+ try {
37
+ const vmstat = execSync('vm_stat', { encoding: 'utf-8', timeout: 2000 });
38
+ const pageSize = parseInt(vmstat.match(/page size of (\d+)/)?.[1] || '4096', 10);
39
+ const wired = parseInt(vmstat.match(/Pages wired down:\s+(\d+)/)?.[1] || '0', 10);
40
+ const active = parseInt(vmstat.match(/Pages active:\s+(\d+)/)?.[1] || '0', 10);
41
+ const compressed = parseInt(vmstat.match(/Pages occupied by compressor:\s+(\d+)/)?.[1] || '0', 10);
42
+ usedMem = (wired + active + compressed) * pageSize;
43
+ }
44
+ catch {
45
+ usedMem = totalMem - freemem();
46
+ }
47
+ }
48
+ else {
49
+ usedMem = totalMem - freemem();
50
+ }
51
+ // CPU load average (1 min) as percentage (rough approximation)
52
+ const load = loadavg()[0];
53
+ const cpuPercent = Math.min(100, Math.round((load / CPU_COUNT) * 100));
54
+ return {
55
+ cpu: cpuPercent,
56
+ memory: {
57
+ usedMB: Math.round(usedMem / (1024 * 1024)),
58
+ totalMB: Math.round(totalMem / (1024 * 1024)),
59
+ percent: Math.round((usedMem / totalMem) * 100),
60
+ },
61
+ };
62
+ }
63
+ catch {
64
+ return {
65
+ cpu: 0,
66
+ memory: { usedMB: 0, totalMB: 0, percent: 0 },
67
+ };
68
+ }
69
+ }
70
+ export function registerSystemRoutes(app, ctx) {
71
+ const windowStatesPath = join(homedir(), '.codeman', 'subagent-window-states.json');
72
+ const parentMapPath = join(homedir(), '.codeman', 'subagent-parents.json');
73
+ // ═══════════════════════════════════════════════════════════════
74
+ // System Status & Health
75
+ // ═══════════════════════════════════════════════════════════════
76
+ // ========== Status ==========
77
+ app.get('/api/status', async () => ctx.getLightState());
78
+ // ========== Tunnel ==========
79
+ app.get('/api/tunnel/status', async () => ctx.tunnelManager.getStatus());
80
+ app.get('/api/tunnel/qr', async (_req, reply) => {
81
+ const url = ctx.tunnelManager.getUrl();
82
+ if (!url) {
83
+ return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'Tunnel not running'));
84
+ }
85
+ try {
86
+ const authPassword = process.env.CODEMAN_PASSWORD;
87
+ if (authPassword) {
88
+ // Auth enabled — use cached SVG with embedded short code
89
+ const svg = await ctx.tunnelManager.getQrSvg(url);
90
+ return { svg, authEnabled: true };
91
+ }
92
+ // No auth — just encode the raw tunnel URL
93
+ const QRCode = await import('qrcode');
94
+ const svg = await QRCode.toString(url, { type: 'svg', margin: 2, width: 256 });
95
+ return { svg, authEnabled: false };
96
+ }
97
+ catch (err) {
98
+ return reply.code(500).send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err)));
99
+ }
100
+ });
101
+ // ═══════════════════════════════════════════════════════════════
102
+ // Authentication (QR auth, session revocation)
103
+ // ═══════════════════════════════════════════════════════════════
104
+ // ========== QR Auth Route ==========
105
+ app.get('/q/:code', async (req, reply) => {
106
+ const shortCode = req.params.code;
107
+ const authPassword = process.env.CODEMAN_PASSWORD;
108
+ // No point if auth isn't enabled — just redirect
109
+ if (!authPassword) {
110
+ return reply.redirect('/');
111
+ }
112
+ const clientIp = req.ip;
113
+ // Per-IP rate limit (separate counter from Basic Auth failures)
114
+ const qrFailures = ctx.qrAuthFailures?.get(clientIp) ?? 0;
115
+ if (qrFailures >= QR_AUTH_FAILURE_MAX) {
116
+ return reply.code(429).send('Too Many Requests');
117
+ }
118
+ // Validate and atomically consume the token
119
+ if (!shortCode || !ctx.tunnelManager.consumeToken(shortCode)) {
120
+ ctx.qrAuthFailures?.set(clientIp, qrFailures + 1);
121
+ return reply.code(401).send('Invalid or expired QR code');
122
+ }
123
+ // Issue session cookie (same pattern as Basic Auth success path)
124
+ const sessionToken = randomBytes(32).toString('hex');
125
+ const clientUA = req.headers['user-agent'] ?? '';
126
+ ctx.authSessions?.set(sessionToken, {
127
+ ip: clientIp,
128
+ ua: clientUA,
129
+ createdAt: Date.now(),
130
+ method: 'qr',
131
+ });
132
+ ctx.qrAuthFailures?.delete(clientIp);
133
+ // Audit log
134
+ const lifecycleLog = getLifecycleLog();
135
+ lifecycleLog.log({
136
+ event: 'qr_auth',
137
+ sessionId: 'system',
138
+ extra: {
139
+ ip: clientIp,
140
+ ua: clientUA,
141
+ shortCodePrefix: shortCode.slice(0, 3) + '***',
142
+ },
143
+ });
144
+ reply.setCookie(AUTH_COOKIE_NAME, sessionToken, {
145
+ httpOnly: true,
146
+ secure: ctx.https,
147
+ sameSite: 'lax',
148
+ maxAge: AUTH_SESSION_TTL_MS / 1000,
149
+ path: '/',
150
+ });
151
+ // Broadcast auth notification — desktop sees who authenticated
152
+ ctx.broadcast(SseEvent.TunnelQrAuthUsed, {
153
+ ip: clientIp,
154
+ ua: clientUA,
155
+ timestamp: Date.now(),
156
+ });
157
+ return reply.redirect('/');
158
+ });
159
+ // ========== QR Regeneration ==========
160
+ app.post('/api/tunnel/qr/regenerate', async () => {
161
+ ctx.tunnelManager.regenerateQrToken();
162
+ return { success: true };
163
+ });
164
+ // ========== Auth Session Revocation ==========
165
+ app.post('/api/auth/revoke', async (req) => {
166
+ const result = RevokeSessionSchema.safeParse(req.body);
167
+ if (result.success && result.data.sessionToken) {
168
+ ctx.authSessions?.delete(result.data.sessionToken);
169
+ }
170
+ else {
171
+ // Revoke all sessions (nuclear option)
172
+ ctx.authSessions?.clear();
173
+ }
174
+ return { success: true };
175
+ });
176
+ // ═══════════════════════════════════════════════════════════════
177
+ // CLI Integrations (OpenCode)
178
+ // ═══════════════════════════════════════════════════════════════
179
+ // ========== OpenCode ==========
180
+ app.get('/api/opencode/status', async () => {
181
+ const { isOpenCodeAvailable, resolveOpenCodeDir } = await import('../../utils/opencode-cli-resolver.js');
182
+ return {
183
+ available: isOpenCodeAvailable(),
184
+ path: resolveOpenCodeDir(),
185
+ };
186
+ });
187
+ // ═══════════════════════════════════════════════════════════════
188
+ // State & Lifecycle (cleanup, lifecycle log, stats)
189
+ // ═══════════════════════════════════════════════════════════════
190
+ // ========== State & Lifecycle ==========
191
+ app.post('/api/cleanup-state', async () => {
192
+ const activeSessionIds = new Set(ctx.sessions.keys());
193
+ const result = ctx.store.cleanupStaleSessions(activeSessionIds);
194
+ const lifecycleLog = getLifecycleLog();
195
+ for (const s of result.cleaned) {
196
+ lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
197
+ }
198
+ return { success: true, cleanedSessions: result.count };
199
+ });
200
+ app.get('/api/session-lifecycle', async (req) => {
201
+ const query = req.query;
202
+ const lifecycleLog = getLifecycleLog();
203
+ const entries = await lifecycleLog.query({
204
+ sessionId: query.sessionId,
205
+ event: query.event,
206
+ since: query.since ? Number(query.since) : undefined,
207
+ limit: query.limit ? Math.min(Number(query.limit), 1000) : 200,
208
+ });
209
+ return { success: true, entries };
210
+ });
211
+ // ========== Stats ==========
212
+ app.get('/api/stats', async () => {
213
+ const activeSessionTokens = {};
214
+ for (const [sessionId, session] of ctx.sessions) {
215
+ activeSessionTokens[sessionId] = {
216
+ inputTokens: session.inputTokens,
217
+ outputTokens: session.outputTokens,
218
+ totalCost: session.totalCost,
219
+ };
220
+ }
221
+ return {
222
+ success: true,
223
+ stats: ctx.store.getAggregateStats(activeSessionTokens),
224
+ raw: ctx.store.getGlobalStats(),
225
+ };
226
+ });
227
+ app.get('/api/token-stats', async () => {
228
+ const activeSessionTokens = {};
229
+ for (const [sessionId, session] of ctx.sessions) {
230
+ activeSessionTokens[sessionId] = {
231
+ inputTokens: session.inputTokens,
232
+ outputTokens: session.outputTokens,
233
+ totalCost: session.totalCost,
234
+ };
235
+ }
236
+ return {
237
+ success: true,
238
+ daily: ctx.store.getDailyStats(30),
239
+ totals: ctx.store.getAggregateStats(activeSessionTokens),
240
+ };
241
+ });
242
+ // ═══════════════════════════════════════════════════════════════
243
+ // Configuration & Settings (config, settings, model config, CPU priority)
244
+ // ═══════════════════════════════════════════════════════════════
245
+ // ========== Config ==========
246
+ app.get('/api/config', async () => {
247
+ return { success: true, config: ctx.store.getConfig() };
248
+ });
249
+ app.put('/api/config', async (req) => {
250
+ const parseResult = ConfigUpdateSchema.safeParse(req.body);
251
+ if (!parseResult.success) {
252
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Invalid config: ${parseResult.error.message}`);
253
+ }
254
+ ctx.store.setConfig(parseResult.data);
255
+ return { success: true, config: ctx.store.getConfig() };
256
+ });
257
+ // ========== Debug/Memory ==========
258
+ app.get('/api/debug/memory', async () => {
259
+ const mem = process.memoryUsage();
260
+ const subagentStats = subagentWatcher.getStats();
261
+ const serverMapSizes = {
262
+ sessions: ctx.sessions.size,
263
+ runSummaryTrackers: ctx.runSummaryTrackers.size,
264
+ scheduledRuns: ctx.scheduledRuns.size,
265
+ activePlanOrchestrators: ctx.activePlanOrchestrators.size,
266
+ };
267
+ const totalServerMapEntries = Object.values(serverMapSizes).reduce((a, b) => a + b, 0);
268
+ const totalSubagentMapEntries = Object.values(subagentStats).reduce((a, b) => a + b, 0);
269
+ return {
270
+ memory: {
271
+ rss: mem.rss,
272
+ rssMB: Math.round((mem.rss / 1024 / 1024) * 10) / 10,
273
+ heapUsed: mem.heapUsed,
274
+ heapUsedMB: Math.round((mem.heapUsed / 1024 / 1024) * 10) / 10,
275
+ heapTotal: mem.heapTotal,
276
+ heapTotalMB: Math.round((mem.heapTotal / 1024 / 1024) * 10) / 10,
277
+ external: mem.external,
278
+ externalMB: Math.round((mem.external / 1024 / 1024) * 10) / 10,
279
+ arrayBuffers: mem.arrayBuffers,
280
+ arrayBuffersMB: Math.round((mem.arrayBuffers / 1024 / 1024) * 10) / 10,
281
+ },
282
+ mapSizes: {
283
+ server: serverMapSizes,
284
+ subagentWatcher: subagentStats,
285
+ totals: {
286
+ serverEntries: totalServerMapEntries,
287
+ subagentEntries: totalSubagentMapEntries,
288
+ allEntries: totalServerMapEntries + totalSubagentMapEntries,
289
+ },
290
+ },
291
+ watchers: {
292
+ fileDebouncers: subagentStats.fileDebouncerCount,
293
+ dirWatchers: subagentStats.dirWatcherCount,
294
+ total: subagentStats.fileDebouncerCount + subagentStats.dirWatcherCount,
295
+ },
296
+ timers: {
297
+ subagentIdleTimers: subagentStats.idleTimerCount,
298
+ total: subagentStats.idleTimerCount,
299
+ },
300
+ uptime: {
301
+ seconds: Math.round(process.uptime()),
302
+ formatted: formatUptime(process.uptime()),
303
+ },
304
+ timestamp: Date.now(),
305
+ };
306
+ });
307
+ // ========== System Stats ==========
308
+ app.get('/api/system/stats', async () => {
309
+ return getSystemStats();
310
+ });
311
+ // ========== Settings ==========
312
+ app.get('/api/settings', async () => {
313
+ try {
314
+ const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
315
+ return JSON.parse(content);
316
+ }
317
+ catch (err) {
318
+ if (err.code !== 'ENOENT') {
319
+ console.error('Failed to read settings:', err);
320
+ }
321
+ }
322
+ return {};
323
+ });
324
+ app.put('/api/settings', async (req) => {
325
+ const settingsResult = SettingsUpdateSchema.safeParse(req.body);
326
+ if (!settingsResult.success) {
327
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid settings');
328
+ }
329
+ const settings = settingsResult.data;
330
+ try {
331
+ const dir = dirname(SETTINGS_PATH);
332
+ if (!existsSync(dir)) {
333
+ mkdirSync(dir, { recursive: true });
334
+ }
335
+ let existing = {};
336
+ try {
337
+ existing = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
338
+ }
339
+ catch {
340
+ /* ignore */
341
+ }
342
+ const merged = { ...existing, ...settings };
343
+ await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
344
+ // Handle subagent tracking toggle dynamically
345
+ const subagentEnabled = settings.subagentTrackingEnabled ?? true;
346
+ if (subagentEnabled && !subagentWatcher.isRunning()) {
347
+ subagentWatcher.start();
348
+ console.log('Subagent watcher started via settings change');
349
+ }
350
+ else if (!subagentEnabled && subagentWatcher.isRunning()) {
351
+ subagentWatcher.stop();
352
+ console.log('Subagent watcher stopped via settings change');
353
+ }
354
+ // Handle image watcher toggle dynamically
355
+ const imageWatcherEnabled = settings.imageWatcherEnabled ?? false;
356
+ if (imageWatcherEnabled && !imageWatcher.isRunning()) {
357
+ imageWatcher.start();
358
+ // Re-watch all active sessions that have image watcher enabled
359
+ for (const session of ctx.sessions.values()) {
360
+ if (session.imageWatcherEnabled) {
361
+ imageWatcher.watchSession(session.id, session.workingDir);
362
+ }
363
+ }
364
+ console.log('Image watcher started via settings change');
365
+ }
366
+ else if (!imageWatcherEnabled && imageWatcher.isRunning()) {
367
+ imageWatcher.stop();
368
+ console.log('Image watcher stopped via settings change');
369
+ }
370
+ // Handle tunnel toggle dynamically
371
+ if ('tunnelEnabled' in settings) {
372
+ const tunnelEnabled = settings.tunnelEnabled;
373
+ if (tunnelEnabled && !ctx.tunnelManager.isRunning()) {
374
+ ctx.tunnelManager.start(ctx.port, ctx.https);
375
+ console.log('Tunnel started via settings change');
376
+ }
377
+ else if (tunnelEnabled && ctx.tunnelManager.isRunning() && ctx.tunnelManager.getUrl()) {
378
+ // Tunnel already running — re-emit so the client gets the URL
379
+ ctx.broadcast(SseEvent.TunnelStarted, { url: ctx.tunnelManager.getUrl() });
380
+ console.log('Tunnel already running, re-broadcast URL to client');
381
+ }
382
+ else if (!tunnelEnabled && ctx.tunnelManager.isRunning()) {
383
+ ctx.tunnelManager.stop();
384
+ console.log('Tunnel stopped via settings change');
385
+ }
386
+ }
387
+ return { success: true };
388
+ }
389
+ catch (err) {
390
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
391
+ }
392
+ });
393
+ // ========== Model Configuration ==========
394
+ app.get('/api/execution/model-config', async () => {
395
+ try {
396
+ const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
397
+ const settings = JSON.parse(content);
398
+ return { success: true, data: settings.modelConfig || {} };
399
+ }
400
+ catch (err) {
401
+ if (err.code !== 'ENOENT') {
402
+ console.error('Failed to read model config:', err);
403
+ }
404
+ return { success: true, data: {} };
405
+ }
406
+ });
407
+ app.put('/api/execution/model-config', async (req) => {
408
+ const mcResult = ModelConfigUpdateSchema.safeParse(req.body);
409
+ if (!mcResult.success) {
410
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid model config');
411
+ }
412
+ const modelConfig = mcResult.data;
413
+ try {
414
+ let existingSettings = {};
415
+ try {
416
+ const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
417
+ existingSettings = JSON.parse(content);
418
+ }
419
+ catch {
420
+ // File doesn't exist yet, start fresh
421
+ }
422
+ existingSettings.modelConfig = modelConfig;
423
+ const dir = dirname(SETTINGS_PATH);
424
+ if (!existsSync(dir)) {
425
+ mkdirSync(dir, { recursive: true });
426
+ }
427
+ await fs.writeFile(SETTINGS_PATH, JSON.stringify(existingSettings, null, 2));
428
+ return { success: true };
429
+ }
430
+ catch (err) {
431
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
432
+ }
433
+ });
434
+ // ========== CPU Priority ==========
435
+ app.get('/api/sessions/:id/cpu-limit', async (req) => {
436
+ const { id } = req.params;
437
+ const session = findSessionOrFail(ctx, id);
438
+ return {
439
+ success: true,
440
+ nice: session.niceConfig,
441
+ };
442
+ });
443
+ app.post('/api/sessions/:id/cpu-limit', async (req) => {
444
+ const { id } = req.params;
445
+ const session = findSessionOrFail(ctx, id);
446
+ const clResult = CpuLimitSchema.safeParse(req.body);
447
+ if (!clResult.success) {
448
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
449
+ }
450
+ const body = clResult.data;
451
+ session.setNice(body);
452
+ ctx.persistSessionState(session);
453
+ ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
454
+ return {
455
+ success: true,
456
+ nice: session.niceConfig,
457
+ note: 'Nice priority only affects newly created mux sessions, not currently running ones.',
458
+ };
459
+ });
460
+ // ═══════════════════════════════════════════════════════════════
461
+ // Subagent Management (window states, parents, monitoring, transcripts)
462
+ // ═══════════════════════════════════════════════════════════════
463
+ // ========== Subagent Window State Persistence ==========
464
+ app.get('/api/subagent-window-states', async () => {
465
+ try {
466
+ const content = await fs.readFile(windowStatesPath, 'utf-8');
467
+ return JSON.parse(content);
468
+ }
469
+ catch (err) {
470
+ if (err.code !== 'ENOENT') {
471
+ console.error('Failed to read subagent window states:', err);
472
+ }
473
+ }
474
+ return { minimized: {}, open: [] };
475
+ });
476
+ app.put('/api/subagent-window-states', async (req) => {
477
+ const swResult = SubagentWindowStatesSchema.safeParse(req.body);
478
+ if (!swResult.success) {
479
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid window states');
480
+ }
481
+ const states = swResult.data;
482
+ try {
483
+ const dir = dirname(windowStatesPath);
484
+ if (!existsSync(dir)) {
485
+ mkdirSync(dir, { recursive: true });
486
+ }
487
+ await fs.writeFile(windowStatesPath, JSON.stringify(states, null, 2));
488
+ return { success: true };
489
+ }
490
+ catch (err) {
491
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
492
+ }
493
+ });
494
+ // ========== Subagent Parent Associations ==========
495
+ app.get('/api/subagent-parents', async () => {
496
+ try {
497
+ const content = await fs.readFile(parentMapPath, 'utf-8');
498
+ return JSON.parse(content);
499
+ }
500
+ catch (err) {
501
+ if (err.code !== 'ENOENT') {
502
+ console.error('Failed to read subagent parent map:', err);
503
+ }
504
+ }
505
+ return {};
506
+ });
507
+ app.put('/api/subagent-parents', async (req) => {
508
+ const spResult = SubagentParentMapSchema.safeParse(req.body);
509
+ if (!spResult.success) {
510
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid parent map');
511
+ }
512
+ const parentMap = spResult.data;
513
+ try {
514
+ const dir = dirname(parentMapPath);
515
+ if (!existsSync(dir)) {
516
+ mkdirSync(dir, { recursive: true });
517
+ }
518
+ await fs.writeFile(parentMapPath, JSON.stringify(parentMap, null, 2));
519
+ return { success: true };
520
+ }
521
+ catch (err) {
522
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
523
+ }
524
+ });
525
+ // ========== Subagent Monitoring ==========
526
+ app.get('/api/subagents', async (req) => {
527
+ const { minutes } = req.query;
528
+ const subagents = minutes
529
+ ? subagentWatcher.getRecentSubagents(parseInt(minutes, 10))
530
+ : subagentWatcher.getSubagents();
531
+ return { success: true, data: subagents };
532
+ });
533
+ app.get('/api/sessions/:id/subagents', async (req) => {
534
+ const { id } = req.params;
535
+ const session = findSessionOrFail(ctx, id);
536
+ const subagents = subagentWatcher.getSubagentsForSession(session.workingDir);
537
+ return { success: true, data: subagents };
538
+ });
539
+ app.get('/api/subagents/:agentId', async (req) => {
540
+ const { agentId } = req.params;
541
+ const info = subagentWatcher.getSubagent(agentId);
542
+ if (!info) {
543
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, `Subagent ${agentId} not found`);
544
+ }
545
+ return { success: true, data: info };
546
+ });
547
+ app.get('/api/subagents/:agentId/transcript', async (req) => {
548
+ const { agentId } = req.params;
549
+ const { limit, format } = req.query;
550
+ const limitNum = limit ? parseInt(limit, 10) : undefined;
551
+ const transcript = await subagentWatcher.getTranscript(agentId, limitNum);
552
+ if (format === 'formatted') {
553
+ const formatted = subagentWatcher.formatTranscript(transcript);
554
+ return { success: true, data: { formatted, entryCount: transcript.length } };
555
+ }
556
+ return { success: true, data: transcript };
557
+ });
558
+ app.delete('/api/subagents/:agentId', async (req) => {
559
+ const { agentId } = req.params;
560
+ const info = subagentWatcher.getSubagent(agentId);
561
+ if (!info) {
562
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Subagent not found');
563
+ }
564
+ const killed = await subagentWatcher.killSubagent(agentId);
565
+ if (killed) {
566
+ return { success: true, data: { agentId, status: 'killed' } };
567
+ }
568
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Subagent not found or already completed');
569
+ });
570
+ app.post('/api/subagents/cleanup', async () => {
571
+ const removed = subagentWatcher.cleanupNow();
572
+ return { success: true, data: { removed, remaining: subagentWatcher.getSubagents().length } };
573
+ });
574
+ app.delete('/api/subagents', async () => {
575
+ const cleared = subagentWatcher.clearAll();
576
+ return { success: true, data: { cleared } };
577
+ });
578
+ // ═══════════════════════════════════════════════════════════════
579
+ // Screenshots (upload, list, serve)
580
+ // ═══════════════════════════════════════════════════════════════
581
+ // ========== Screenshots ==========
582
+ app.post('/api/screenshots', async (req, reply) => {
583
+ const contentType = req.headers['content-type'] ?? '';
584
+ if (!contentType.includes('multipart/form-data')) {
585
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Expected multipart/form-data');
586
+ }
587
+ // Parse multipart boundary
588
+ const boundaryMatch = contentType.match(/boundary=(.+?)(?:;|$)/);
589
+ if (!boundaryMatch) {
590
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing boundary');
591
+ }
592
+ // Collect raw body
593
+ const chunks = [];
594
+ let totalSize = 0;
595
+ for await (const chunk of req.raw) {
596
+ totalSize += chunk.length;
597
+ if (totalSize > MAX_SCREENSHOT_SIZE) {
598
+ reply.status(413);
599
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'File too large (max 10MB)');
600
+ }
601
+ chunks.push(chunk);
602
+ }
603
+ const body = Buffer.concat(chunks);
604
+ // Extract file from multipart body
605
+ const boundary = '--' + boundaryMatch[1];
606
+ const boundaryBuf = Buffer.from(boundary);
607
+ const parts = [];
608
+ let pos = 0;
609
+ // Find each part between boundaries
610
+ while (pos < body.length) {
611
+ const start = body.indexOf(boundaryBuf, pos);
612
+ if (start === -1)
613
+ break;
614
+ const afterBoundary = start + boundaryBuf.length;
615
+ // Check for closing boundary (--)
616
+ if (body[afterBoundary] === 0x2d && body[afterBoundary + 1] === 0x2d)
617
+ break;
618
+ // Skip \r\n after boundary
619
+ const headerStart = afterBoundary + 2;
620
+ const headerEnd = body.indexOf(Buffer.from('\r\n\r\n'), headerStart);
621
+ if (headerEnd === -1)
622
+ break;
623
+ const headers = body.subarray(headerStart, headerEnd).toString();
624
+ const dataStart = headerEnd + 4;
625
+ const nextBoundary = body.indexOf(boundaryBuf, dataStart);
626
+ // Data ends 2 bytes before next boundary (\r\n)
627
+ const dataEnd = nextBoundary === -1 ? body.length : nextBoundary - 2;
628
+ parts.push({ headers, data: body.subarray(dataStart, dataEnd) });
629
+ pos = nextBoundary === -1 ? body.length : nextBoundary;
630
+ }
631
+ const filePart = parts.find((p) => p.headers.includes('name="file"'));
632
+ if (!filePart || filePart.data.length === 0) {
633
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'No file uploaded');
634
+ }
635
+ // Determine extension from Content-Type or filename
636
+ let ext = '.png';
637
+ const filenameMatch = filePart.headers.match(/filename="(.+?)"/);
638
+ if (filenameMatch) {
639
+ const origExt = filenameMatch[1].match(/\.(png|jpg|jpeg|webp|gif)$/i);
640
+ if (origExt)
641
+ ext = origExt[0].toLowerCase();
642
+ }
643
+ const ctMatch = filePart.headers.match(/Content-Type:\s*image\/(png|jpeg|webp|gif)/i);
644
+ if (ctMatch) {
645
+ const map = {
646
+ png: '.png',
647
+ jpeg: '.jpg',
648
+ webp: '.webp',
649
+ gif: '.gif',
650
+ };
651
+ ext = map[ctMatch[1].toLowerCase()] ?? ext;
652
+ }
653
+ // Save to ~/.codeman/screenshots/
654
+ if (!existsSync(SCREENSHOTS_DIR)) {
655
+ mkdirSync(SCREENSHOTS_DIR, { recursive: true });
656
+ }
657
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
658
+ const filename = `screenshot_${timestamp}${ext}`;
659
+ const filepath = join(SCREENSHOTS_DIR, filename);
660
+ await fs.writeFile(filepath, filePart.data);
661
+ return { success: true, path: filepath, filename };
662
+ });
663
+ app.get('/api/screenshots', async () => {
664
+ if (!existsSync(SCREENSHOTS_DIR)) {
665
+ return { files: [] };
666
+ }
667
+ const files = readdirSync(SCREENSHOTS_DIR)
668
+ .filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f))
669
+ .sort()
670
+ .reverse()
671
+ .slice(0, 50)
672
+ .map((name) => ({ name, path: join(SCREENSHOTS_DIR, name) }));
673
+ return { files };
674
+ });
675
+ app.get('/api/screenshots/:name', async (req, reply) => {
676
+ const { name } = req.params;
677
+ // Prevent path traversal
678
+ if (name.includes('/') || name.includes('\\') || name.includes('..')) {
679
+ reply.status(400);
680
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid filename');
681
+ }
682
+ const filepath = join(SCREENSHOTS_DIR, name);
683
+ if (!existsSync(filepath)) {
684
+ reply.status(404);
685
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Screenshot not found');
686
+ }
687
+ const ext = name.match(/\.(png|jpg|jpeg|webp|gif)$/i)?.[1]?.toLowerCase() ?? 'png';
688
+ const mimeMap = {
689
+ png: 'image/png',
690
+ jpg: 'image/jpeg',
691
+ jpeg: 'image/jpeg',
692
+ webp: 'image/webp',
693
+ gif: 'image/gif',
694
+ };
695
+ reply.type(mimeMap[ext] ?? 'image/png');
696
+ return fs.readFile(filepath);
697
+ });
698
+ }
699
+ //# sourceMappingURL=system-routes.js.map