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,38 @@
1
+ /**
2
+ * @fileoverview Shared helper functions for route modules.
3
+ *
4
+ * Contains pure functions extracted from server.ts and a session lookup helper
5
+ * that replaces ~43 inline not-found checks across route handlers.
6
+ */
7
+ import { Session } from '../session.js';
8
+ import type { SessionPort } from './ports/session-port.js';
9
+ import type { EventPort } from './ports/event-port.js';
10
+ export declare const CASES_DIR: string;
11
+ export declare const SETTINGS_PATH: string;
12
+ /**
13
+ * Look up a session by ID or throw a structured error.
14
+ * Replaces the pattern: `const session = sessions.get(id); if (!session) return createErrorResponse(...)`.
15
+ */
16
+ export declare function findSessionOrFail(ctx: SessionPort, sessionId: string): Session;
17
+ /**
18
+ * Formats uptime in seconds to a human-readable string (e.g., "1d 2h 30m 15s").
19
+ */
20
+ export declare function formatUptime(seconds: number): string;
21
+ /**
22
+ * Sanitizes hook event data before broadcasting via SSE.
23
+ * Extracts only relevant fields and limits total size to prevent
24
+ * oversized payloads from being broadcast to all connected clients.
25
+ */
26
+ export declare function sanitizeHookData(data: Record<string, unknown> | null | undefined): Record<string, unknown>;
27
+ /**
28
+ * Auto-configure Ralph tracker for a session.
29
+ *
30
+ * Priority order:
31
+ * 1. .claude/ralph-loop.local.md (official Ralph Wiggum plugin state)
32
+ * 2. CLAUDE.md <promise> tags (fallback)
33
+ *
34
+ * The ralph-loop.local.md file has priority because it contains
35
+ * the exact configuration from an active Ralph loop session.
36
+ */
37
+ export declare function autoConfigureRalph(session: Session, workingDir: string, ctx: EventPort): void;
38
+ //# sourceMappingURL=route-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../src/web/route-helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,eAAO,MAAM,SAAS,QAAmC,CAAC;AAC1D,eAAO,MAAM,aAAa,QAA+C,CAAC;AAK1E;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAapD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2C1G;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAsC7F"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @fileoverview Shared helper functions for route modules.
3
+ *
4
+ * Contains pure functions extracted from server.ts and a session lookup helper
5
+ * that replaces ~43 inline not-found checks across route handlers.
6
+ */
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { ApiErrorCode, createErrorResponse } from '../types.js';
10
+ import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
11
+ import { SseEvent } from './sse-events.js';
12
+ // Shared path constants used across route modules
13
+ export const CASES_DIR = join(homedir(), 'codeman-cases');
14
+ export const SETTINGS_PATH = join(homedir(), '.codeman', 'settings.json');
15
+ // Maximum hook data size (prevents oversized SSE broadcasts)
16
+ const MAX_HOOK_DATA_SIZE = 8 * 1024;
17
+ /**
18
+ * Look up a session by ID or throw a structured error.
19
+ * Replaces the pattern: `const session = sessions.get(id); if (!session) return createErrorResponse(...)`.
20
+ */
21
+ export function findSessionOrFail(ctx, sessionId) {
22
+ const session = ctx.sessions.get(sessionId);
23
+ if (!session) {
24
+ throw Object.assign(new Error(`Session ${sessionId} not found`), {
25
+ statusCode: 404,
26
+ body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`),
27
+ });
28
+ }
29
+ return session;
30
+ }
31
+ /**
32
+ * Formats uptime in seconds to a human-readable string (e.g., "1d 2h 30m 15s").
33
+ */
34
+ export function formatUptime(seconds) {
35
+ const days = Math.floor(seconds / 86400);
36
+ const hours = Math.floor((seconds % 86400) / 3600);
37
+ const minutes = Math.floor((seconds % 3600) / 60);
38
+ const secs = Math.floor(seconds % 60);
39
+ const parts = [];
40
+ if (days > 0)
41
+ parts.push(`${days}d`);
42
+ if (hours > 0)
43
+ parts.push(`${hours}h`);
44
+ if (minutes > 0)
45
+ parts.push(`${minutes}m`);
46
+ if (secs > 0 || parts.length === 0)
47
+ parts.push(`${secs}s`);
48
+ return parts.join(' ');
49
+ }
50
+ /**
51
+ * Sanitizes hook event data before broadcasting via SSE.
52
+ * Extracts only relevant fields and limits total size to prevent
53
+ * oversized payloads from being broadcast to all connected clients.
54
+ */
55
+ export function sanitizeHookData(data) {
56
+ if (!data || typeof data !== 'object')
57
+ return {};
58
+ // Only forward known safe fields from Claude Code hook stdin
59
+ const safeFields = {};
60
+ const allowedKeys = [
61
+ 'hook_event_name',
62
+ 'tool_name',
63
+ 'tool_input',
64
+ 'session_id',
65
+ 'cwd',
66
+ 'permission_mode',
67
+ 'stop_hook_active',
68
+ 'transcript_path',
69
+ ];
70
+ for (const key of allowedKeys) {
71
+ if (key in data && data[key] !== undefined) {
72
+ safeFields[key] = data[key];
73
+ }
74
+ }
75
+ // For tool_input, extract only summary fields (not full file content)
76
+ if (safeFields.tool_input && typeof safeFields.tool_input === 'object') {
77
+ const input = safeFields.tool_input;
78
+ const summary = {};
79
+ if (input.command)
80
+ summary.command = String(input.command).slice(0, 500);
81
+ if (input.file_path)
82
+ summary.file_path = String(input.file_path).slice(0, 500);
83
+ if (input.description)
84
+ summary.description = String(input.description).slice(0, 200);
85
+ if (input.query)
86
+ summary.query = String(input.query).slice(0, 200);
87
+ if (input.url)
88
+ summary.url = String(input.url).slice(0, 500);
89
+ if (input.pattern)
90
+ summary.pattern = String(input.pattern).slice(0, 200);
91
+ if (input.prompt)
92
+ summary.prompt = String(input.prompt).slice(0, 200);
93
+ safeFields.tool_input = summary;
94
+ }
95
+ // Final size check - drop if serialized data exceeds limit
96
+ const serialized = JSON.stringify(safeFields);
97
+ if (serialized.length > MAX_HOOK_DATA_SIZE) {
98
+ return { tool_name: safeFields.tool_name, _truncated: true };
99
+ }
100
+ return safeFields;
101
+ }
102
+ /**
103
+ * Auto-configure Ralph tracker for a session.
104
+ *
105
+ * Priority order:
106
+ * 1. .claude/ralph-loop.local.md (official Ralph Wiggum plugin state)
107
+ * 2. CLAUDE.md <promise> tags (fallback)
108
+ *
109
+ * The ralph-loop.local.md file has priority because it contains
110
+ * the exact configuration from an active Ralph loop session.
111
+ */
112
+ export function autoConfigureRalph(session, workingDir, ctx) {
113
+ // First, try to read the official Ralph Wiggum plugin state file
114
+ const ralphConfig = parseRalphLoopConfig(workingDir);
115
+ if (ralphConfig && ralphConfig.completionPromise) {
116
+ session.ralphTracker.enable();
117
+ session.ralphTracker.startLoop(ralphConfig.completionPromise, ralphConfig.maxIterations ?? undefined);
118
+ // Restore iteration count if available
119
+ if (ralphConfig.iteration > 0) {
120
+ // The tracker's cycleCount will be updated when we detect iteration patterns
121
+ // in the terminal output, but we can set maxIterations now
122
+ console.log(`[auto-detect] Ralph loop at iteration ${ralphConfig.iteration}/${ralphConfig.maxIterations ?? '∞'}`);
123
+ }
124
+ console.log(`[auto-detect] Configured Ralph loop for session ${session.id} from ralph-loop.local.md: ${ralphConfig.completionPromise}`);
125
+ ctx.broadcast(SseEvent.SessionRalphLoopUpdate, {
126
+ sessionId: session.id,
127
+ state: session.ralphTracker.loopState,
128
+ });
129
+ return;
130
+ }
131
+ // Fallback: try CLAUDE.md
132
+ const claudeMdPath = join(workingDir, 'CLAUDE.md');
133
+ const completionPhrase = extractCompletionPhrase(claudeMdPath);
134
+ if (completionPhrase) {
135
+ session.ralphTracker.enable();
136
+ session.ralphTracker.startLoop(completionPhrase);
137
+ console.log(`[auto-detect] Configured Ralph loop for session ${session.id} from CLAUDE.md: ${completionPhrase}`);
138
+ ctx.broadcast(SseEvent.SessionRalphLoopUpdate, {
139
+ sessionId: session.id,
140
+ state: session.ralphTracker.loopState,
141
+ });
142
+ }
143
+ }
144
+ //# sourceMappingURL=route-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-helpers.js","sourceRoot":"","sources":["../../src/web/route-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Case management routes.
3
+ * Handles CRUD for cases (directories under ~/codeman-cases and linked folders),
4
+ * fix-plan reading, and ralph-wizard file serving.
5
+ */
6
+ import { FastifyInstance } from 'fastify';
7
+ import type { EventPort, ConfigPort } from '../ports/index.js';
8
+ export declare function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & ConfigPort): void;
9
+ //# sourceMappingURL=case-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"case-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/case-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAY1C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,IAAI,CAod1F"}
@@ -0,0 +1,426 @@
1
+ /**
2
+ * @fileoverview Case management routes.
3
+ * Handles CRUD for cases (directories under ~/codeman-cases and linked folders),
4
+ * fix-plan reading, and ralph-wizard file serving.
5
+ */
6
+ import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
7
+ import fs from 'node:fs/promises';
8
+ import { join, resolve, relative, isAbsolute } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
11
+ import { CreateCaseSchema, LinkCaseSchema } from '../schemas.js';
12
+ import { generateClaudeMd } from '../../templates/claude-md.js';
13
+ import { writeHooksConfig } from '../../hooks-config.js';
14
+ import { CASES_DIR } from '../route-helpers.js';
15
+ import { SseEvent } from '../sse-events.js';
16
+ export function registerCaseRoutes(app, ctx) {
17
+ // ═══════════════════════════════════════════════════════════════
18
+ // Case CRUD (list, create, link, detail, fix-plan)
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // ========== List Cases ==========
21
+ app.get('/api/cases', async () => {
22
+ const cases = [];
23
+ // Get cases from CASES_DIR
24
+ try {
25
+ const entries = await fs.readdir(CASES_DIR, { withFileTypes: true });
26
+ for (const e of entries) {
27
+ if (e.isDirectory()) {
28
+ cases.push({
29
+ name: e.name,
30
+ path: join(CASES_DIR, e.name),
31
+ hasClaudeMd: existsSync(join(CASES_DIR, e.name, 'CLAUDE.md')),
32
+ });
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ // CASES_DIR may not exist yet
38
+ }
39
+ // Get linked cases
40
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
41
+ try {
42
+ const linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
43
+ for (const [name, path] of Object.entries(linkedCases)) {
44
+ // Only add if not already in cases (avoid duplicates) and path exists
45
+ if (!cases.some((c) => c.name === name) && existsSync(path)) {
46
+ cases.push({
47
+ name,
48
+ path,
49
+ hasClaudeMd: existsSync(join(path, 'CLAUDE.md')),
50
+ });
51
+ }
52
+ }
53
+ }
54
+ catch (err) {
55
+ if (err.code !== 'ENOENT') {
56
+ console.warn('[Server] Failed to read linked cases:', err);
57
+ }
58
+ }
59
+ return cases;
60
+ });
61
+ app.post('/api/cases', async (req) => {
62
+ const result = CreateCaseSchema.safeParse(req.body);
63
+ if (!result.success) {
64
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
65
+ }
66
+ const { name, description } = result.data;
67
+ const casePath = join(CASES_DIR, name);
68
+ // Security: Path traversal protection - use relative path check
69
+ const resolvedPath = resolve(casePath);
70
+ const resolvedBase = resolve(CASES_DIR);
71
+ const relPath = relative(resolvedBase, resolvedPath);
72
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
73
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
74
+ }
75
+ if (existsSync(casePath)) {
76
+ return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'Case already exists');
77
+ }
78
+ try {
79
+ mkdirSync(casePath, { recursive: true });
80
+ mkdirSync(join(casePath, 'src'), { recursive: true });
81
+ // Read settings to get custom template path
82
+ const templatePath = await ctx.getDefaultClaudeMdPath();
83
+ const claudeMd = generateClaudeMd(name, description || '', templatePath);
84
+ writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
85
+ // Write .claude/settings.local.json with hooks for desktop notifications
86
+ await writeHooksConfig(casePath);
87
+ ctx.broadcast(SseEvent.CaseCreated, { name, path: casePath });
88
+ return { success: true, data: { case: { name, path: casePath } } };
89
+ }
90
+ catch (err) {
91
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
92
+ }
93
+ });
94
+ // Link an existing folder as a case
95
+ app.post('/api/cases/link', async (req) => {
96
+ const lcResult = LinkCaseSchema.safeParse(req.body);
97
+ if (!lcResult.success) {
98
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
99
+ }
100
+ const { name, path: folderPath } = lcResult.data;
101
+ // Expand ~ to home directory
102
+ const expandedPath = folderPath.startsWith('~') ? join(homedir(), folderPath.slice(1)) : folderPath;
103
+ // Validate the folder exists
104
+ if (!existsSync(expandedPath)) {
105
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, `Folder not found: ${expandedPath}`);
106
+ }
107
+ // Check if case name already exists in CASES_DIR
108
+ const casePath = join(CASES_DIR, name);
109
+ if (existsSync(casePath)) {
110
+ return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
111
+ }
112
+ // Load existing linked cases
113
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
114
+ let linkedCases = {};
115
+ try {
116
+ linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
117
+ }
118
+ catch (err) {
119
+ if (err.code !== 'ENOENT') {
120
+ console.warn('[Server] Failed to read linked cases:', err);
121
+ }
122
+ }
123
+ // Check if name is already linked
124
+ if (linkedCases[name]) {
125
+ return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `Case "${name}" is already linked to ${linkedCases[name]}`);
126
+ }
127
+ // Save the linked case
128
+ linkedCases[name] = expandedPath;
129
+ try {
130
+ const codemanDir = join(homedir(), '.codeman');
131
+ if (!existsSync(codemanDir)) {
132
+ mkdirSync(codemanDir, { recursive: true });
133
+ }
134
+ await fs.writeFile(linkedCasesFile, JSON.stringify(linkedCases, null, 2));
135
+ ctx.broadcast(SseEvent.CaseLinked, { name, path: expandedPath });
136
+ return { success: true, data: { case: { name, path: expandedPath } } };
137
+ }
138
+ catch (err) {
139
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
140
+ }
141
+ });
142
+ app.get('/api/cases/:name', async (req) => {
143
+ const { name } = req.params;
144
+ // Security: Path traversal protection
145
+ const resolvedPath = resolve(join(CASES_DIR, name));
146
+ const resolvedBase = resolve(CASES_DIR);
147
+ const relPath = relative(resolvedBase, resolvedPath);
148
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
149
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
150
+ }
151
+ // First check linked cases
152
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
153
+ try {
154
+ const linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
155
+ if (linkedCases[name]) {
156
+ const linkedPath = linkedCases[name];
157
+ return {
158
+ name,
159
+ path: linkedPath,
160
+ hasClaudeMd: existsSync(join(linkedPath, 'CLAUDE.md')),
161
+ linked: true,
162
+ };
163
+ }
164
+ }
165
+ catch {
166
+ // ENOENT or parse errors - fall through to CASES_DIR check
167
+ }
168
+ // Then check CASES_DIR
169
+ const casePath = join(CASES_DIR, name);
170
+ if (!existsSync(casePath)) {
171
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Case not found');
172
+ }
173
+ return {
174
+ name,
175
+ path: casePath,
176
+ hasClaudeMd: existsSync(join(casePath, 'CLAUDE.md')),
177
+ };
178
+ });
179
+ // Read @fix_plan.md from a case directory (for wizard to detect existing plans)
180
+ app.get('/api/cases/:name/fix-plan', async (req) => {
181
+ const { name } = req.params;
182
+ // Security: Path traversal protection
183
+ const resolvedPath = resolve(join(CASES_DIR, name));
184
+ const resolvedBase = resolve(CASES_DIR);
185
+ const relPath = relative(resolvedBase, resolvedPath);
186
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
187
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
188
+ }
189
+ // Get case path (check linked cases first, then CASES_DIR)
190
+ let casePath = null;
191
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
192
+ try {
193
+ const linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
194
+ if (linkedCases[name]) {
195
+ casePath = linkedCases[name];
196
+ }
197
+ }
198
+ catch {
199
+ // ENOENT or parse errors - fall through to CASES_DIR
200
+ }
201
+ if (!casePath) {
202
+ casePath = join(CASES_DIR, name);
203
+ }
204
+ const fixPlanPath = join(casePath, '@fix_plan.md');
205
+ if (!existsSync(fixPlanPath)) {
206
+ return { success: true, exists: false, content: null, todos: [] };
207
+ }
208
+ try {
209
+ const content = await fs.readFile(fixPlanPath, 'utf-8');
210
+ // Parse todos from the content (similar to ralph-tracker's importFixPlanMarkdown)
211
+ const todos = [];
212
+ const todoPattern = /^-\s*\[([ xX-])\]\s*(.+)$/;
213
+ const p0HeaderPattern = /^##\s*(High Priority|Critical|P0|Critical Path)/i;
214
+ const p1HeaderPattern = /^##\s*(Standard|P1|Medium Priority)/i;
215
+ const p2HeaderPattern = /^##\s*(Nice to Have|P2|Low Priority)/i;
216
+ const completedHeaderPattern = /^##\s*Completed/i;
217
+ let currentPriority = null;
218
+ let inCompletedSection = false;
219
+ for (const line of content.split('\n')) {
220
+ const trimmed = line.trim();
221
+ if (p0HeaderPattern.test(trimmed)) {
222
+ currentPriority = 'P0';
223
+ inCompletedSection = false;
224
+ continue;
225
+ }
226
+ if (p1HeaderPattern.test(trimmed)) {
227
+ currentPriority = 'P1';
228
+ inCompletedSection = false;
229
+ continue;
230
+ }
231
+ if (p2HeaderPattern.test(trimmed)) {
232
+ currentPriority = 'P2';
233
+ inCompletedSection = false;
234
+ continue;
235
+ }
236
+ if (completedHeaderPattern.test(trimmed)) {
237
+ inCompletedSection = true;
238
+ continue;
239
+ }
240
+ const match = trimmed.match(todoPattern);
241
+ if (match) {
242
+ const [, checkboxState, taskContent] = match;
243
+ let status;
244
+ if (inCompletedSection || checkboxState === 'x' || checkboxState === 'X') {
245
+ status = 'completed';
246
+ }
247
+ else if (checkboxState === '-') {
248
+ status = 'in_progress';
249
+ }
250
+ else {
251
+ status = 'pending';
252
+ }
253
+ todos.push({
254
+ content: taskContent.trim(),
255
+ status,
256
+ priority: inCompletedSection ? null : currentPriority,
257
+ });
258
+ }
259
+ }
260
+ // Calculate stats in a single pass for better performance
261
+ let pending = 0, inProgress = 0, completed = 0;
262
+ for (const t of todos) {
263
+ if (t.status === 'pending')
264
+ pending++;
265
+ else if (t.status === 'in_progress')
266
+ inProgress++;
267
+ else if (t.status === 'completed')
268
+ completed++;
269
+ }
270
+ const stats = { total: todos.length, pending, inProgress, completed };
271
+ return {
272
+ success: true,
273
+ exists: true,
274
+ content,
275
+ todos,
276
+ stats,
277
+ };
278
+ }
279
+ catch (err) {
280
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to read @fix_plan.md: ${err}`);
281
+ }
282
+ });
283
+ // ═══════════════════════════════════════════════════════════════
284
+ // Ralph Wizard Files (per-case prompt/result serving)
285
+ // ═══════════════════════════════════════════════════════════════
286
+ // ========== List Wizard Files ==========
287
+ app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => {
288
+ const { caseName } = req.params;
289
+ let casePath = join(CASES_DIR, caseName);
290
+ // Security: Path traversal protection - use relative path check
291
+ const resolvedCase = resolve(casePath);
292
+ const resolvedBase = resolve(CASES_DIR);
293
+ const relPath = relative(resolvedBase, resolvedCase);
294
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
295
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
296
+ }
297
+ // Check linked cases if path doesn't exist
298
+ if (!existsSync(casePath)) {
299
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
300
+ try {
301
+ const linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
302
+ if (linkedCases[caseName]) {
303
+ casePath = linkedCases[caseName];
304
+ }
305
+ }
306
+ catch {
307
+ // No linked cases file
308
+ }
309
+ }
310
+ const wizardDir = join(casePath, 'ralph-wizard');
311
+ if (!existsSync(wizardDir)) {
312
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Ralph wizard directory not found');
313
+ }
314
+ // List all subdirectories and their files
315
+ const files = [];
316
+ const entries = readdirSync(wizardDir, { withFileTypes: true });
317
+ for (const entry of entries) {
318
+ if (entry.isDirectory()) {
319
+ const agentDir = join(wizardDir, entry.name);
320
+ const agentFiles = {
321
+ agentType: entry.name,
322
+ };
323
+ if (existsSync(join(agentDir, 'prompt.md'))) {
324
+ agentFiles.promptFile = `${entry.name}/prompt.md`;
325
+ }
326
+ if (existsSync(join(agentDir, 'result.json'))) {
327
+ agentFiles.resultFile = `${entry.name}/result.json`;
328
+ }
329
+ if (agentFiles.promptFile || agentFiles.resultFile) {
330
+ files.push(agentFiles);
331
+ }
332
+ }
333
+ }
334
+ return { success: true, data: { files, caseName } };
335
+ });
336
+ // Read a specific ralph-wizard file
337
+ // Cache disabled to ensure fresh prompts when starting new plan generations
338
+ app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
339
+ const { caseName, filePath } = req.params;
340
+ let casePath = join(CASES_DIR, caseName);
341
+ // Prevent browser caching - prompts change between plan generations
342
+ reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
343
+ reply.header('Pragma', 'no-cache');
344
+ reply.header('Expires', '0');
345
+ // Security: Path traversal protection for case name - use relative path check
346
+ const resolvedCase = resolve(casePath);
347
+ const resolvedBase = resolve(CASES_DIR);
348
+ const relPath = relative(resolvedBase, resolvedCase);
349
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
350
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
351
+ }
352
+ // Check linked cases if path doesn't exist
353
+ if (!existsSync(casePath)) {
354
+ const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
355
+ try {
356
+ const linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
357
+ if (linkedCases[caseName]) {
358
+ casePath = linkedCases[caseName];
359
+ }
360
+ }
361
+ catch {
362
+ // No linked cases file
363
+ }
364
+ }
365
+ const wizardDir = join(casePath, 'ralph-wizard');
366
+ // Decode the file path (it may be URL encoded)
367
+ const decodedPath = decodeURIComponent(filePath);
368
+ const fullPath = join(wizardDir, decodedPath);
369
+ // Security: ensure path is within wizard directory
370
+ const resolvedPath = resolve(fullPath);
371
+ const resolvedWizard = resolve(wizardDir);
372
+ if (!resolvedPath.startsWith(resolvedWizard)) {
373
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid file path');
374
+ }
375
+ let content;
376
+ try {
377
+ content = await fs.readFile(fullPath, 'utf-8');
378
+ }
379
+ catch (err) {
380
+ if (err.code === 'ENOENT') {
381
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
382
+ }
383
+ throw err;
384
+ }
385
+ const isJson = filePath.endsWith('.json');
386
+ // Parse JSON content safely (may contain invalid JSON or unescaped control characters)
387
+ let parsed = null;
388
+ if (isJson) {
389
+ try {
390
+ parsed = JSON.parse(content);
391
+ }
392
+ catch {
393
+ // Try repairing common JSON issues (unescaped control characters, trailing commas)
394
+ try {
395
+ let repaired = content;
396
+ // Fix trailing commas before closing brackets
397
+ repaired = repaired.replace(/,(\s*[\]}])/g, '$1');
398
+ // Fix unescaped control characters within JSON strings
399
+ repaired = repaired.replace(/"([^"\\]|\\.)*"/g, (match) => {
400
+ return match
401
+ .replace(/\n/g, '\\n')
402
+ .replace(/\r/g, '\\r')
403
+ .replace(/\t/g, '\\t')
404
+ .replace(
405
+ // eslint-disable-next-line no-control-regex
406
+ /[\x00-\x1f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
407
+ });
408
+ parsed = JSON.parse(repaired);
409
+ }
410
+ catch {
411
+ // Still invalid - return null for parsed, content available as raw string
412
+ }
413
+ }
414
+ }
415
+ return {
416
+ success: true,
417
+ data: {
418
+ content,
419
+ filePath: decodedPath,
420
+ isJson,
421
+ parsed,
422
+ },
423
+ };
424
+ });
425
+ }
426
+ //# sourceMappingURL=case-routes.js.map