aicodeman 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/README.md +91 -0
  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 +36 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +51 -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.map +1 -1
  45. package/dist/hooks-config.js +7 -6
  46. package/dist/hooks-config.js.map +1 -1
  47. package/dist/image-watcher.d.ts +4 -4
  48. package/dist/image-watcher.d.ts.map +1 -1
  49. package/dist/image-watcher.js +17 -30
  50. package/dist/image-watcher.js.map +1 -1
  51. package/dist/index.js +1 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/plan-orchestrator.d.ts +2 -24
  54. package/dist/plan-orchestrator.d.ts.map +1 -1
  55. package/dist/plan-orchestrator.js.map +1 -1
  56. package/dist/push-store.d.ts +1 -1
  57. package/dist/push-store.d.ts.map +1 -1
  58. package/dist/push-store.js +4 -12
  59. package/dist/push-store.js.map +1 -1
  60. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  61. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  62. package/dist/ralph-fix-plan-watcher.js +326 -0
  63. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  64. package/dist/ralph-plan-tracker.d.ts +201 -0
  65. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  66. package/dist/ralph-plan-tracker.js +325 -0
  67. package/dist/ralph-plan-tracker.js.map +1 -0
  68. package/dist/ralph-stall-detector.d.ts +84 -0
  69. package/dist/ralph-stall-detector.d.ts.map +1 -0
  70. package/dist/ralph-stall-detector.js +139 -0
  71. package/dist/ralph-stall-detector.js.map +1 -0
  72. package/dist/ralph-status-parser.d.ts +141 -0
  73. package/dist/ralph-status-parser.d.ts.map +1 -0
  74. package/dist/ralph-status-parser.js +478 -0
  75. package/dist/ralph-status-parser.js.map +1 -0
  76. package/dist/ralph-tracker.d.ts +194 -685
  77. package/dist/ralph-tracker.d.ts.map +1 -1
  78. package/dist/ralph-tracker.js +349 -1713
  79. package/dist/ralph-tracker.js.map +1 -1
  80. package/dist/respawn-adaptive-timing.d.ts +61 -0
  81. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  82. package/dist/respawn-adaptive-timing.js +105 -0
  83. package/dist/respawn-adaptive-timing.js.map +1 -0
  84. package/dist/respawn-controller.d.ts +12 -101
  85. package/dist/respawn-controller.d.ts.map +1 -1
  86. package/dist/respawn-controller.js +144 -593
  87. package/dist/respawn-controller.js.map +1 -1
  88. package/dist/respawn-health.d.ts +54 -0
  89. package/dist/respawn-health.d.ts.map +1 -0
  90. package/dist/respawn-health.js +183 -0
  91. package/dist/respawn-health.js.map +1 -0
  92. package/dist/respawn-metrics.d.ts +81 -0
  93. package/dist/respawn-metrics.d.ts.map +1 -0
  94. package/dist/respawn-metrics.js +198 -0
  95. package/dist/respawn-metrics.js.map +1 -0
  96. package/dist/respawn-patterns.d.ts +45 -0
  97. package/dist/respawn-patterns.d.ts.map +1 -0
  98. package/dist/respawn-patterns.js +125 -0
  99. package/dist/respawn-patterns.js.map +1 -0
  100. package/dist/session-auto-ops.d.ts +89 -0
  101. package/dist/session-auto-ops.d.ts.map +1 -0
  102. package/dist/session-auto-ops.js +224 -0
  103. package/dist/session-auto-ops.js.map +1 -0
  104. package/dist/session-cli-builder.d.ts +62 -0
  105. package/dist/session-cli-builder.d.ts.map +1 -0
  106. package/dist/session-cli-builder.js +121 -0
  107. package/dist/session-cli-builder.js.map +1 -0
  108. package/dist/session-task-cache.d.ts +52 -0
  109. package/dist/session-task-cache.d.ts.map +1 -0
  110. package/dist/session-task-cache.js +90 -0
  111. package/dist/session-task-cache.js.map +1 -0
  112. package/dist/session.d.ts +2 -33
  113. package/dist/session.d.ts.map +1 -1
  114. package/dist/session.js +58 -309
  115. package/dist/session.js.map +1 -1
  116. package/dist/state-store.d.ts +2 -2
  117. package/dist/state-store.d.ts.map +1 -1
  118. package/dist/state-store.js +12 -23
  119. package/dist/state-store.js.map +1 -1
  120. package/dist/subagent-watcher.d.ts +3 -4
  121. package/dist/subagent-watcher.d.ts.map +1 -1
  122. package/dist/subagent-watcher.js +24 -61
  123. package/dist/subagent-watcher.js.map +1 -1
  124. package/dist/team-watcher.d.ts.map +1 -1
  125. package/dist/team-watcher.js +2 -5
  126. package/dist/team-watcher.js.map +1 -1
  127. package/dist/tmux-manager.d.ts.map +1 -1
  128. package/dist/tmux-manager.js +1 -2
  129. package/dist/tmux-manager.js.map +1 -1
  130. package/dist/tunnel-manager.d.ts +26 -0
  131. package/dist/tunnel-manager.d.ts.map +1 -1
  132. package/dist/tunnel-manager.js +127 -7
  133. package/dist/tunnel-manager.js.map +1 -1
  134. package/dist/types/api.d.ts +93 -0
  135. package/dist/types/api.d.ts.map +1 -0
  136. package/dist/types/api.js +83 -0
  137. package/dist/types/api.js.map +1 -0
  138. package/dist/types/app-state.d.ts +100 -0
  139. package/dist/types/app-state.d.ts.map +1 -0
  140. package/dist/types/app-state.js +59 -0
  141. package/dist/types/app-state.js.map +1 -0
  142. package/dist/types/common.d.ts +70 -0
  143. package/dist/types/common.d.ts.map +1 -0
  144. package/dist/types/common.js +8 -0
  145. package/dist/types/common.js.map +1 -0
  146. package/dist/types/index.d.ts +18 -0
  147. package/dist/types/index.d.ts.map +1 -0
  148. package/dist/types/index.js +18 -0
  149. package/dist/types/index.js.map +1 -0
  150. package/dist/types/lifecycle.d.ts +17 -0
  151. package/dist/types/lifecycle.d.ts.map +1 -0
  152. package/dist/types/lifecycle.js +5 -0
  153. package/dist/types/lifecycle.js.map +1 -0
  154. package/dist/types/plan.d.ts +32 -0
  155. package/dist/types/plan.d.ts.map +1 -0
  156. package/dist/types/plan.js +5 -0
  157. package/dist/types/plan.js.map +1 -0
  158. package/dist/types/push.d.ts +23 -0
  159. package/dist/types/push.d.ts.map +1 -0
  160. package/dist/types/push.js +5 -0
  161. package/dist/types/push.js.map +1 -0
  162. package/dist/types/ralph.d.ts +241 -0
  163. package/dist/types/ralph.d.ts.map +1 -0
  164. package/dist/types/ralph.js +49 -0
  165. package/dist/types/ralph.js.map +1 -0
  166. package/dist/types/respawn.d.ts +250 -0
  167. package/dist/types/respawn.d.ts.map +1 -0
  168. package/dist/types/respawn.js +5 -0
  169. package/dist/types/respawn.js.map +1 -0
  170. package/dist/types/run-summary.d.ts +81 -0
  171. package/dist/types/run-summary.d.ts.map +1 -0
  172. package/dist/types/run-summary.js +22 -0
  173. package/dist/types/run-summary.js.map +1 -0
  174. package/dist/types/session.d.ts +130 -0
  175. package/dist/types/session.d.ts.map +1 -0
  176. package/dist/types/session.js +5 -0
  177. package/dist/types/session.js.map +1 -0
  178. package/dist/types/task.d.ts +58 -0
  179. package/dist/types/task.d.ts.map +1 -0
  180. package/dist/types/task.js +5 -0
  181. package/dist/types/task.js.map +1 -0
  182. package/dist/types/teams.d.ts +55 -0
  183. package/dist/types/teams.d.ts.map +1 -0
  184. package/dist/types/teams.js +5 -0
  185. package/dist/types/teams.js.map +1 -0
  186. package/dist/types/tools.d.ts +46 -0
  187. package/dist/types/tools.d.ts.map +1 -0
  188. package/dist/types/tools.js +5 -0
  189. package/dist/types/tools.js.map +1 -0
  190. package/dist/types.d.ts +1 -1138
  191. package/dist/types.d.ts.map +1 -1
  192. package/dist/types.js +1 -214
  193. package/dist/types.js.map +1 -1
  194. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  195. package/dist/utils/claude-cli-resolver.js +1 -2
  196. package/dist/utils/claude-cli-resolver.js.map +1 -1
  197. package/dist/utils/debouncer.d.ts +111 -0
  198. package/dist/utils/debouncer.d.ts.map +1 -0
  199. package/dist/utils/debouncer.js +162 -0
  200. package/dist/utils/debouncer.js.map +1 -0
  201. package/dist/utils/index.d.ts +3 -2
  202. package/dist/utils/index.d.ts.map +1 -1
  203. package/dist/utils/index.js +3 -2
  204. package/dist/utils/index.js.map +1 -1
  205. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  206. package/dist/utils/opencode-cli-resolver.js +1 -2
  207. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  208. package/dist/utils/string-similarity.d.ts +0 -57
  209. package/dist/utils/string-similarity.d.ts.map +1 -1
  210. package/dist/utils/string-similarity.js +3 -18
  211. package/dist/utils/string-similarity.js.map +1 -1
  212. package/dist/web/middleware/auth.d.ts +31 -0
  213. package/dist/web/middleware/auth.d.ts.map +1 -0
  214. package/dist/web/middleware/auth.js +154 -0
  215. package/dist/web/middleware/auth.js.map +1 -0
  216. package/dist/web/ports/auth-port.d.ts +18 -0
  217. package/dist/web/ports/auth-port.d.ts.map +1 -0
  218. package/dist/web/ports/auth-port.js +6 -0
  219. package/dist/web/ports/auth-port.js.map +1 -0
  220. package/dist/web/ports/config-port.d.ts +28 -0
  221. package/dist/web/ports/config-port.d.ts.map +1 -0
  222. package/dist/web/ports/config-port.js +6 -0
  223. package/dist/web/ports/config-port.js.map +1 -0
  224. package/dist/web/ports/event-port.d.ts +13 -0
  225. package/dist/web/ports/event-port.d.ts.map +1 -0
  226. package/dist/web/ports/event-port.js +6 -0
  227. package/dist/web/ports/event-port.js.map +1 -0
  228. package/dist/web/ports/index.d.ts +14 -0
  229. package/dist/web/ports/index.d.ts.map +1 -0
  230. package/dist/web/ports/index.js +9 -0
  231. package/dist/web/ports/index.js.map +1 -0
  232. package/dist/web/ports/infra-port.d.ts +36 -0
  233. package/dist/web/ports/infra-port.d.ts.map +1 -0
  234. package/dist/web/ports/infra-port.js +6 -0
  235. package/dist/web/ports/infra-port.js.map +1 -0
  236. package/dist/web/ports/respawn-port.d.ts +20 -0
  237. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  238. package/dist/web/ports/respawn-port.js +6 -0
  239. package/dist/web/ports/respawn-port.js.map +1 -0
  240. package/dist/web/ports/session-port.d.ts +15 -0
  241. package/dist/web/ports/session-port.d.ts.map +1 -0
  242. package/dist/web/ports/session-port.js +6 -0
  243. package/dist/web/ports/session-port.js.map +1 -0
  244. package/dist/web/public/api-client.js +70 -0
  245. package/dist/web/public/api-client.js.br +0 -0
  246. package/dist/web/public/api-client.js.gz +0 -0
  247. package/dist/web/public/app.js +151 -235
  248. package/dist/web/public/app.js.br +0 -0
  249. package/dist/web/public/app.js.gz +0 -0
  250. package/dist/web/public/constants.js +238 -0
  251. package/dist/web/public/constants.js.br +0 -0
  252. package/dist/web/public/constants.js.gz +0 -0
  253. package/dist/web/public/index.html +11 -3
  254. package/dist/web/public/index.html.br +0 -0
  255. package/dist/web/public/index.html.gz +0 -0
  256. package/dist/web/public/keyboard-accessory.js +279 -0
  257. package/dist/web/public/keyboard-accessory.js.br +0 -0
  258. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  259. package/dist/web/public/mobile-handlers.js +467 -0
  260. package/dist/web/public/mobile-handlers.js.br +0 -0
  261. package/dist/web/public/mobile-handlers.js.gz +0 -0
  262. package/dist/web/public/mobile.css.gz +0 -0
  263. package/dist/web/public/notification-manager.js +445 -0
  264. package/dist/web/public/notification-manager.js.br +0 -0
  265. package/dist/web/public/notification-manager.js.gz +0 -0
  266. package/dist/web/public/ralph-wizard.js +3 -3
  267. package/dist/web/public/ralph-wizard.js.br +0 -0
  268. package/dist/web/public/ralph-wizard.js.gz +0 -0
  269. package/dist/web/public/styles.css.gz +0 -0
  270. package/dist/web/public/subagent-windows.js +1115 -0
  271. package/dist/web/public/subagent-windows.js.br +0 -0
  272. package/dist/web/public/subagent-windows.js.gz +0 -0
  273. package/dist/web/public/sw.js.gz +0 -0
  274. package/dist/web/public/upload.html.gz +0 -0
  275. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  276. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  277. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  278. package/dist/web/public/vendor/xterm.css.gz +0 -0
  279. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  280. package/dist/web/public/voice-input.js +858 -0
  281. package/dist/web/public/voice-input.js.br +0 -0
  282. package/dist/web/public/voice-input.js.gz +0 -0
  283. package/dist/web/route-helpers.d.ts +38 -0
  284. package/dist/web/route-helpers.d.ts.map +1 -0
  285. package/dist/web/route-helpers.js +143 -0
  286. package/dist/web/route-helpers.js.map +1 -0
  287. package/dist/web/routes/case-routes.d.ts +9 -0
  288. package/dist/web/routes/case-routes.d.ts.map +1 -0
  289. package/dist/web/routes/case-routes.js +419 -0
  290. package/dist/web/routes/case-routes.js.map +1 -0
  291. package/dist/web/routes/file-routes.d.ts +8 -0
  292. package/dist/web/routes/file-routes.d.ts.map +1 -0
  293. package/dist/web/routes/file-routes.js +337 -0
  294. package/dist/web/routes/file-routes.js.map +1 -0
  295. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  296. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  297. package/dist/web/routes/hook-event-routes.js +57 -0
  298. package/dist/web/routes/hook-event-routes.js.map +1 -0
  299. package/dist/web/routes/index.d.ts +16 -0
  300. package/dist/web/routes/index.d.ts.map +1 -0
  301. package/dist/web/routes/index.js +16 -0
  302. package/dist/web/routes/index.js.map +1 -0
  303. package/dist/web/routes/mux-routes.d.ts +8 -0
  304. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  305. package/dist/web/routes/mux-routes.js +32 -0
  306. package/dist/web/routes/mux-routes.js.map +1 -0
  307. package/dist/web/routes/plan-routes.d.ts +9 -0
  308. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  309. package/dist/web/routes/plan-routes.js +381 -0
  310. package/dist/web/routes/plan-routes.js.map +1 -0
  311. package/dist/web/routes/push-routes.d.ts +8 -0
  312. package/dist/web/routes/push-routes.d.ts.map +1 -0
  313. package/dist/web/routes/push-routes.js +49 -0
  314. package/dist/web/routes/push-routes.js.map +1 -0
  315. package/dist/web/routes/ralph-routes.d.ts +9 -0
  316. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  317. package/dist/web/routes/ralph-routes.js +475 -0
  318. package/dist/web/routes/ralph-routes.js.map +1 -0
  319. package/dist/web/routes/respawn-routes.d.ts +8 -0
  320. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  321. package/dist/web/routes/respawn-routes.js +260 -0
  322. package/dist/web/routes/respawn-routes.js.map +1 -0
  323. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  324. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  325. package/dist/web/routes/scheduled-routes.js +51 -0
  326. package/dist/web/routes/scheduled-routes.js.map +1 -0
  327. package/dist/web/routes/session-routes.d.ts +9 -0
  328. package/dist/web/routes/session-routes.d.ts.map +1 -0
  329. package/dist/web/routes/session-routes.js +729 -0
  330. package/dist/web/routes/session-routes.js.map +1 -0
  331. package/dist/web/routes/system-routes.d.ts +9 -0
  332. package/dist/web/routes/system-routes.d.ts.map +1 -0
  333. package/dist/web/routes/system-routes.js +678 -0
  334. package/dist/web/routes/system-routes.js.map +1 -0
  335. package/dist/web/routes/team-routes.d.ts +8 -0
  336. package/dist/web/routes/team-routes.d.ts.map +1 -0
  337. package/dist/web/routes/team-routes.js +14 -0
  338. package/dist/web/routes/team-routes.js.map +1 -0
  339. package/dist/web/schemas.d.ts +43 -3
  340. package/dist/web/schemas.d.ts.map +1 -1
  341. package/dist/web/schemas.js +6 -2
  342. package/dist/web/schemas.js.map +1 -1
  343. package/dist/web/server.d.ts +10 -9
  344. package/dist/web/server.d.ts.map +1 -1
  345. package/dist/web/server.js +335 -3824
  346. package/dist/web/server.js.map +1 -1
  347. package/package.json +1 -1
@@ -0,0 +1,729 @@
1
+ /**
2
+ * @fileoverview Session management routes.
3
+ * Covers session CRUD, input/output, terminal buffer, quick-start, quick-run,
4
+ * auto-clear, auto-compact, image watcher, flicker filter, and logout.
5
+ */
6
+ import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
7
+ import { existsSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
8
+ import fs from 'node:fs/promises';
9
+ import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types.js';
10
+ import { Session } from '../../session.js';
11
+ import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, } from '../schemas.js';
12
+ import { autoConfigureRalph, CASES_DIR, SETTINGS_PATH } from '../route-helpers.js';
13
+ import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
14
+ import { writeHooksConfig, updateCaseEnvVars } from '../../hooks-config.js';
15
+ import { generateClaudeMd } from '../../templates/claude-md.js';
16
+ import { imageWatcher } from '../../image-watcher.js';
17
+ import { getLifecycleLog } from '../../session-lifecycle-log.js';
18
+ import { MAX_CONCURRENT_SESSIONS } from '../../config/map-limits.js';
19
+ import { RunSummaryTracker } from '../../run-summary.js';
20
+ import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
21
+ // Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
22
+ // eslint-disable-next-line no-control-regex
23
+ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
24
+ // eslint-disable-next-line no-control-regex
25
+ const CTRL_L_PATTERN = /\x0c/g;
26
+ const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
27
+ export function registerSessionRoutes(app, ctx) {
28
+ // ========== Logout ==========
29
+ app.post('/api/logout', async (req, reply) => {
30
+ // Invalidate server-side session token (not just the browser cookie)
31
+ const sessionToken = req.cookies[AUTH_COOKIE_NAME];
32
+ if (sessionToken) {
33
+ ctx.authSessions?.delete(sessionToken);
34
+ }
35
+ reply.clearCookie(AUTH_COOKIE_NAME, { path: '/' });
36
+ return { success: true };
37
+ });
38
+ // ========== Session Listing ==========
39
+ app.get('/api/sessions', async () => {
40
+ return ctx.getLightSessionsState();
41
+ });
42
+ // ========== Session Creation ==========
43
+ app.post('/api/sessions', async (req) => {
44
+ // Prevent unbounded session creation
45
+ if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
46
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached. Delete some sessions first.`);
47
+ }
48
+ const result = CreateSessionSchema.safeParse(req.body);
49
+ if (!result.success) {
50
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
51
+ }
52
+ const body = result.data;
53
+ const workingDir = body.workingDir || process.cwd();
54
+ // Validate workingDir exists and is a directory
55
+ if (body.workingDir) {
56
+ try {
57
+ const stat = statSync(workingDir);
58
+ if (!stat.isDirectory()) {
59
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir is not a directory');
60
+ }
61
+ }
62
+ catch {
63
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir does not exist');
64
+ }
65
+ }
66
+ // Write env overrides to .claude/settings.local.json if provided
67
+ if (body.envOverrides && Object.keys(body.envOverrides).length > 0) {
68
+ await updateCaseEnvVars(workingDir, body.envOverrides);
69
+ }
70
+ // Check OpenCode availability if requested
71
+ if (body.mode === 'opencode') {
72
+ const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js');
73
+ if (!isOpenCodeAvailable()) {
74
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash');
75
+ }
76
+ }
77
+ const globalNice = await ctx.getGlobalNiceConfig();
78
+ const modelConfig = await ctx.getModelConfig();
79
+ const mode = body.mode || 'claude';
80
+ const model = mode === 'opencode' ? body.openCodeConfig?.model : mode !== 'shell' ? modelConfig?.defaultModel : undefined;
81
+ const claudeModeConfig = await ctx.getClaudeModeConfig();
82
+ const session = new Session({
83
+ workingDir,
84
+ mode,
85
+ name: body.name || '',
86
+ mux: ctx.mux,
87
+ useMux: true,
88
+ niceConfig: globalNice,
89
+ model,
90
+ claudeMode: claudeModeConfig.claudeMode,
91
+ allowedTools: claudeModeConfig.allowedTools,
92
+ openCodeConfig: mode === 'opencode' ? body.openCodeConfig : undefined,
93
+ });
94
+ ctx.addSession(session);
95
+ ctx.store.incrementSessionsCreated();
96
+ ctx.persistSessionState(session);
97
+ await ctx.setupSessionListeners(session);
98
+ getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name });
99
+ // Use light state for broadcast + response — buffers are fetched on-demand via /terminal.
100
+ // Avoids serializing 2-3MB of terminal+text buffers per session creation.
101
+ const lightState = ctx.getSessionStateWithRespawn(session);
102
+ ctx.broadcast('session:created', lightState);
103
+ return { success: true, session: lightState };
104
+ });
105
+ // ========== Rename Session ==========
106
+ app.put('/api/sessions/:id/name', async (req) => {
107
+ const { id } = req.params;
108
+ const result = SessionNameSchema.safeParse(req.body);
109
+ if (!result.success) {
110
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
111
+ }
112
+ const body = result.data;
113
+ const session = ctx.sessions.get(id);
114
+ if (!session) {
115
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
116
+ }
117
+ const name = String(body.name || '').slice(0, MAX_SESSION_NAME_LENGTH);
118
+ session.name = name;
119
+ // Also update the mux session name if applicable
120
+ ctx.mux.updateSessionName(id, session.name);
121
+ ctx.persistSessionState(session);
122
+ ctx.broadcast('session:updated', ctx.getSessionStateWithRespawn(session));
123
+ return { success: true, name: session.name };
124
+ });
125
+ // ========== Set Session Color ==========
126
+ app.put('/api/sessions/:id/color', async (req) => {
127
+ const { id } = req.params;
128
+ const result = SessionColorSchema.safeParse(req.body);
129
+ if (!result.success) {
130
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
131
+ }
132
+ const body = result.data;
133
+ const session = ctx.sessions.get(id);
134
+ if (!session) {
135
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
136
+ }
137
+ const validColors = ['default', 'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink'];
138
+ if (!validColors.includes(body.color)) {
139
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid color');
140
+ }
141
+ session.setColor(body.color);
142
+ ctx.persistSessionState(session);
143
+ ctx.broadcast('session:updated', ctx.getSessionStateWithRespawn(session));
144
+ return { success: true, color: session.color };
145
+ });
146
+ // ========== Delete Session ==========
147
+ app.delete('/api/sessions/:id', async (req) => {
148
+ const { id } = req.params;
149
+ const query = req.query;
150
+ const killMux = query.killMux !== 'false'; // Default to true
151
+ if (!ctx.sessions.has(id)) {
152
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
153
+ }
154
+ await ctx.cleanupSession(id, killMux, 'user_delete');
155
+ return { success: true };
156
+ });
157
+ // ========== Delete All Sessions ==========
158
+ app.delete('/api/sessions', async () => {
159
+ const sessionIds = Array.from(ctx.sessions.keys());
160
+ let killed = 0;
161
+ for (const id of sessionIds) {
162
+ if (ctx.sessions.has(id)) {
163
+ await ctx.cleanupSession(id, true, 'user_bulk_delete');
164
+ killed++;
165
+ }
166
+ }
167
+ return { success: true, data: { killed } };
168
+ });
169
+ // ========== Get Session Detail ==========
170
+ app.get('/api/sessions/:id', async (req) => {
171
+ const { id } = req.params;
172
+ const session = ctx.sessions.get(id);
173
+ if (!session) {
174
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
175
+ }
176
+ // Use light state (no full buffers) — terminal buffer available via /terminal endpoint.
177
+ // Full buffers were 2-3MB and caused slowness when polled frequently (e.g. Ralph wizard).
178
+ return ctx.getSessionStateWithRespawn(session);
179
+ });
180
+ // ========== Get Session Output ==========
181
+ app.get('/api/sessions/:id/output', async (req) => {
182
+ const { id } = req.params;
183
+ const session = ctx.sessions.get(id);
184
+ if (!session) {
185
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
186
+ }
187
+ return {
188
+ success: true,
189
+ data: {
190
+ textOutput: session.textOutput,
191
+ messages: session.messages,
192
+ errorBuffer: session.errorBuffer,
193
+ },
194
+ };
195
+ });
196
+ // ========== Get Ralph State ==========
197
+ app.get('/api/sessions/:id/ralph-state', async (req) => {
198
+ const { id } = req.params;
199
+ const session = ctx.sessions.get(id);
200
+ if (!session) {
201
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
202
+ }
203
+ return {
204
+ success: true,
205
+ data: {
206
+ loop: session.ralphLoopState,
207
+ todos: session.ralphTodos,
208
+ todoStats: session.ralphTodoStats,
209
+ },
210
+ };
211
+ });
212
+ // ========== Get Run Summary ==========
213
+ app.get('/api/sessions/:id/run-summary', async (req) => {
214
+ const { id } = req.params;
215
+ const session = ctx.sessions.get(id);
216
+ if (!session) {
217
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
218
+ }
219
+ const tracker = ctx.runSummaryTrackers.get(id);
220
+ if (!tracker) {
221
+ // Create a fresh tracker if one doesn't exist (shouldn't happen normally)
222
+ const newTracker = new RunSummaryTracker(id, session.name);
223
+ ctx.runSummaryTrackers.set(id, newTracker);
224
+ return { success: true, summary: newTracker.getSummary() };
225
+ }
226
+ // Update session name in case it changed
227
+ tracker.setSessionName(session.name);
228
+ return { success: true, summary: tracker.getSummary() };
229
+ });
230
+ // ========== Get Active Tools ==========
231
+ app.get('/api/sessions/:id/active-tools', async (req) => {
232
+ const { id } = req.params;
233
+ const session = ctx.sessions.get(id);
234
+ if (!session) {
235
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
236
+ }
237
+ return {
238
+ success: true,
239
+ data: {
240
+ tools: session.activeTools,
241
+ },
242
+ };
243
+ });
244
+ // ========== Run Prompt ==========
245
+ app.post('/api/sessions/:id/run', async (req) => {
246
+ const { id } = req.params;
247
+ const result = RunPromptSchema.safeParse(req.body);
248
+ if (!result.success) {
249
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
250
+ }
251
+ const { prompt } = result.data;
252
+ const session = ctx.sessions.get(id);
253
+ if (!session) {
254
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
255
+ }
256
+ if (session.isBusy()) {
257
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
258
+ }
259
+ // Run async, don't wait
260
+ session.runPrompt(prompt).catch((err) => {
261
+ ctx.broadcast('session:error', { id, error: err.message });
262
+ });
263
+ ctx.broadcast('session:running', { id, prompt });
264
+ return { success: true };
265
+ });
266
+ // ========== Start Interactive Mode ==========
267
+ app.post('/api/sessions/:id/interactive', async (req) => {
268
+ const { id } = req.params;
269
+ const session = ctx.sessions.get(id);
270
+ if (!session) {
271
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
272
+ }
273
+ if (session.isBusy()) {
274
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
275
+ }
276
+ try {
277
+ // Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
278
+ // Ralph tracker is not supported for opencode sessions
279
+ if (session.mode !== 'opencode' &&
280
+ ctx.store.getConfig().ralphEnabled &&
281
+ !session.ralphTracker.autoEnableDisabled) {
282
+ autoConfigureRalph(session, session.workingDir, ctx);
283
+ if (!session.ralphTracker.enabled) {
284
+ session.ralphTracker.enable();
285
+ }
286
+ }
287
+ await session.startInteractive();
288
+ getLifecycleLog().log({
289
+ event: 'started',
290
+ sessionId: id,
291
+ name: session.name,
292
+ mode: session.mode,
293
+ });
294
+ ctx.broadcast('session:interactive', { id });
295
+ ctx.broadcast('session:updated', { session: ctx.getSessionStateWithRespawn(session) });
296
+ return { success: true };
297
+ }
298
+ catch (err) {
299
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
300
+ }
301
+ });
302
+ // ========== Start Shell Mode ==========
303
+ app.post('/api/sessions/:id/shell', async (req) => {
304
+ const { id } = req.params;
305
+ const session = ctx.sessions.get(id);
306
+ if (!session) {
307
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
308
+ }
309
+ if (session.isBusy()) {
310
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
311
+ }
312
+ try {
313
+ await session.startShell();
314
+ getLifecycleLog().log({
315
+ event: 'started',
316
+ sessionId: id,
317
+ name: session.name,
318
+ mode: 'shell',
319
+ });
320
+ ctx.broadcast('session:interactive', { id, mode: 'shell' });
321
+ ctx.broadcast('session:updated', { session: ctx.getSessionStateWithRespawn(session) });
322
+ return { success: true };
323
+ }
324
+ catch (err) {
325
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
326
+ }
327
+ });
328
+ // ========== Send Input ==========
329
+ app.post('/api/sessions/:id/input', async (req) => {
330
+ const { id } = req.params;
331
+ const result = SessionInputWithLimitSchema.safeParse(req.body);
332
+ if (!result.success) {
333
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
334
+ }
335
+ const { input, useMux } = result.data;
336
+ const session = ctx.sessions.get(id);
337
+ if (!session) {
338
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
339
+ }
340
+ const inputStr = String(input);
341
+ if (inputStr.length > MAX_INPUT_LENGTH) {
342
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Input exceeds maximum length (${MAX_INPUT_LENGTH} bytes)`);
343
+ }
344
+ // Write input to PTY. Direct write is synchronous; writeViaMux
345
+ // (tmux send-keys) is fire-and-forget to avoid blocking the HTTP response.
346
+ if (useMux) {
347
+ // Fire-and-forget: don't block HTTP response on tmux child process.
348
+ // Fallback to direct write on failure.
349
+ session
350
+ .writeViaMux(inputStr)
351
+ .then((ok) => {
352
+ if (!ok) {
353
+ console.warn(`[Server] writeViaMux failed for session ${id}, falling back to direct write`);
354
+ session.write(inputStr);
355
+ }
356
+ })
357
+ .catch(() => {
358
+ session.write(inputStr);
359
+ });
360
+ }
361
+ else {
362
+ session.write(inputStr);
363
+ }
364
+ return { success: true };
365
+ });
366
+ // ========== Resize Terminal ==========
367
+ app.post('/api/sessions/:id/resize', async (req) => {
368
+ const { id } = req.params;
369
+ const result = ResizeSchema.safeParse(req.body);
370
+ if (!result.success) {
371
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
372
+ }
373
+ const { cols, rows } = result.data;
374
+ const session = ctx.sessions.get(id);
375
+ if (!session) {
376
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
377
+ }
378
+ session.resize(cols, rows);
379
+ return { success: true };
380
+ });
381
+ // ========== Get Terminal Buffer ==========
382
+ // Query params:
383
+ // tail=<bytes> - Only return last N bytes (faster initial load)
384
+ app.get('/api/sessions/:id/terminal', async (req) => {
385
+ const { id } = req.params;
386
+ const query = req.query;
387
+ const session = ctx.sessions.get(id);
388
+ if (!session) {
389
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
390
+ }
391
+ const tailBytes = query.tail ? parseInt(query.tail, 10) : 0;
392
+ const fullSize = session.terminalBufferLength;
393
+ let truncated = false;
394
+ let cleanBuffer;
395
+ if (tailBytes > 0 && fullSize > tailBytes) {
396
+ // Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
397
+ // Banner is near the top and gets discarded by tail anyway.
398
+ cleanBuffer = session.terminalBuffer.slice(-tailBytes);
399
+ truncated = true;
400
+ // Avoid starting mid-ANSI-escape: find first newline within the first 4KB
401
+ // and start from there. This prevents xterm.js from parsing a partial escape
402
+ // sequence which corrupts cursor position for all subsequent Ink redraws.
403
+ const firstNewline = cleanBuffer.indexOf('\n');
404
+ if (firstNewline > 0 && firstNewline < 4096) {
405
+ cleanBuffer = cleanBuffer.slice(firstNewline + 1);
406
+ }
407
+ }
408
+ else {
409
+ // Full buffer: clean junk before actual Claude content
410
+ cleanBuffer = session.terminalBuffer;
411
+ // Find where Claude banner starts (has color codes before "Claude")
412
+ const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
413
+ if (claudeMatch && claudeMatch.index !== undefined && claudeMatch.index > 0) {
414
+ let lineStart = claudeMatch.index;
415
+ while (lineStart > 0 && cleanBuffer[lineStart - 1] !== '\n') {
416
+ lineStart--;
417
+ }
418
+ cleanBuffer = cleanBuffer.slice(lineStart);
419
+ }
420
+ }
421
+ // Remove Ctrl+L and leading whitespace (cheap on tailed subset)
422
+ cleanBuffer = cleanBuffer.replace(CTRL_L_PATTERN, '').replace(LEADING_WHITESPACE_PATTERN, '');
423
+ return {
424
+ terminalBuffer: cleanBuffer,
425
+ status: session.status,
426
+ fullSize,
427
+ truncated,
428
+ };
429
+ });
430
+ // ========== Auto-Clear ==========
431
+ app.post('/api/sessions/:id/auto-clear', async (req) => {
432
+ const { id } = req.params;
433
+ const acResult = AutoClearSchema.safeParse(req.body);
434
+ if (!acResult.success) {
435
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
436
+ }
437
+ const body = acResult.data;
438
+ const session = ctx.sessions.get(id);
439
+ if (!session) {
440
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
441
+ }
442
+ session.setAutoClear(body.enabled, body.threshold);
443
+ ctx.persistSessionState(session);
444
+ ctx.broadcast('session:updated', ctx.getSessionStateWithRespawn(session));
445
+ return {
446
+ success: true,
447
+ data: {
448
+ autoClear: {
449
+ enabled: session.autoClearEnabled,
450
+ threshold: session.autoClearThreshold,
451
+ },
452
+ },
453
+ };
454
+ });
455
+ // ========== Auto-Compact ==========
456
+ app.post('/api/sessions/:id/auto-compact', async (req) => {
457
+ const { id } = req.params;
458
+ const compactResult = AutoCompactSchema.safeParse(req.body);
459
+ if (!compactResult.success) {
460
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
461
+ }
462
+ const body = compactResult.data;
463
+ const session = ctx.sessions.get(id);
464
+ if (!session) {
465
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
466
+ }
467
+ session.setAutoCompact(body.enabled, body.threshold, body.prompt);
468
+ ctx.persistSessionState(session);
469
+ ctx.broadcast('session:updated', ctx.getSessionStateWithRespawn(session));
470
+ return {
471
+ success: true,
472
+ data: {
473
+ autoCompact: {
474
+ enabled: session.autoCompactEnabled,
475
+ threshold: session.autoCompactThreshold,
476
+ prompt: session.autoCompactPrompt,
477
+ },
478
+ },
479
+ };
480
+ });
481
+ // ========== Image Watcher ==========
482
+ app.post('/api/sessions/:id/image-watcher', async (req) => {
483
+ const { id } = req.params;
484
+ const iwResult = ImageWatcherSchema.safeParse(req.body);
485
+ if (!iwResult.success) {
486
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
487
+ }
488
+ const body = iwResult.data;
489
+ const session = ctx.sessions.get(id);
490
+ if (!session) {
491
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
492
+ }
493
+ if (body.enabled) {
494
+ imageWatcher.watchSession(session.id, session.workingDir);
495
+ }
496
+ else {
497
+ imageWatcher.unwatchSession(session.id);
498
+ }
499
+ // Store state on session for persistence
500
+ session.imageWatcherEnabled = body.enabled;
501
+ ctx.persistSessionState(session);
502
+ return {
503
+ success: true,
504
+ data: {
505
+ imageWatcherEnabled: body.enabled,
506
+ },
507
+ };
508
+ });
509
+ // ========== Flicker Filter ==========
510
+ app.post('/api/sessions/:id/flicker-filter', async (req) => {
511
+ const { id } = req.params;
512
+ const ffResult = FlickerFilterSchema.safeParse(req.body);
513
+ if (!ffResult.success) {
514
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
515
+ }
516
+ const body = ffResult.data;
517
+ const session = ctx.sessions.get(id);
518
+ if (!session) {
519
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
520
+ }
521
+ session.flickerFilterEnabled = body.enabled;
522
+ ctx.persistSessionState(session);
523
+ ctx.broadcast('session:updated', ctx.getSessionStateWithRespawn(session));
524
+ return {
525
+ success: true,
526
+ data: {
527
+ flickerFilterEnabled: body.enabled,
528
+ },
529
+ };
530
+ });
531
+ // ========== Quick Run ==========
532
+ app.post('/api/run', async (req) => {
533
+ // Prevent unbounded session creation
534
+ if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
535
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached`);
536
+ }
537
+ const qrResult = QuickRunSchema.safeParse(req.body);
538
+ if (!qrResult.success) {
539
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
540
+ }
541
+ const { prompt, workingDir } = qrResult.data;
542
+ if (!prompt.trim()) {
543
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'prompt is required');
544
+ }
545
+ const dir = workingDir || process.cwd();
546
+ // Validate workingDir exists and is a directory
547
+ if (workingDir) {
548
+ try {
549
+ const stat = statSync(dir);
550
+ if (!stat.isDirectory()) {
551
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir is not a directory');
552
+ }
553
+ }
554
+ catch {
555
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'workingDir does not exist');
556
+ }
557
+ }
558
+ const session = new Session({ workingDir: dir });
559
+ ctx.addSession(session);
560
+ ctx.store.incrementSessionsCreated();
561
+ ctx.persistSessionState(session);
562
+ await ctx.setupSessionListeners(session);
563
+ getLifecycleLog().log({
564
+ event: 'created',
565
+ sessionId: session.id,
566
+ name: session.name,
567
+ reason: 'run_prompt',
568
+ });
569
+ ctx.broadcast('session:created', ctx.getSessionStateWithRespawn(session));
570
+ try {
571
+ const result = await session.runPrompt(prompt);
572
+ // Clean up session after completion to prevent memory leak
573
+ await ctx.cleanupSession(session.id, true, 'run_prompt_complete');
574
+ return { success: true, sessionId: session.id, ...result };
575
+ }
576
+ catch (err) {
577
+ // Clean up session on error too
578
+ await ctx.cleanupSession(session.id, true, 'run_prompt_error');
579
+ return { success: false, sessionId: session.id, error: getErrorMessage(err) };
580
+ }
581
+ });
582
+ // ========== Quick Start ==========
583
+ app.post('/api/quick-start', async (req) => {
584
+ // Prevent unbounded session creation
585
+ if (ctx.sessions.size >= MAX_CONCURRENT_SESSIONS) {
586
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, `Maximum concurrent sessions (${MAX_CONCURRENT_SESSIONS}) reached.`);
587
+ }
588
+ const result = QuickStartSchema.safeParse(req.body);
589
+ if (!result.success) {
590
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
591
+ }
592
+ const { caseName = 'testcase', mode = 'claude', openCodeConfig } = result.data;
593
+ // Check OpenCode availability if requested
594
+ if (mode === 'opencode') {
595
+ const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js');
596
+ if (!isOpenCodeAvailable()) {
597
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash');
598
+ }
599
+ }
600
+ const casePath = join(CASES_DIR, caseName);
601
+ // Security: Path traversal protection - use relative path check
602
+ const resolvedPath = resolve(casePath);
603
+ const resolvedBase = resolve(CASES_DIR);
604
+ const relPath = relative(resolvedBase, resolvedPath);
605
+ if (relPath.startsWith('..') || isAbsolute(relPath)) {
606
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
607
+ }
608
+ // Create case folder and CLAUDE.md if it doesn't exist
609
+ if (!existsSync(casePath)) {
610
+ try {
611
+ mkdirSync(casePath, { recursive: true });
612
+ mkdirSync(join(casePath, 'src'), { recursive: true });
613
+ // Read settings to get custom template path
614
+ const templatePath = await ctx.getDefaultClaudeMdPath();
615
+ const claudeMd = generateClaudeMd(caseName, '', templatePath);
616
+ writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
617
+ // Write .claude/settings.local.json with hooks for desktop notifications
618
+ // (Claude-specific — OpenCode uses its own plugin system)
619
+ if (mode !== 'opencode') {
620
+ await writeHooksConfig(casePath);
621
+ }
622
+ ctx.broadcast('case:created', { name: caseName, path: casePath });
623
+ }
624
+ catch (err) {
625
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to create case: ${getErrorMessage(err)}`);
626
+ }
627
+ }
628
+ // Create a new session with the case as working directory
629
+ // Apply global Nice priority config and model config from settings
630
+ const niceConfig = await ctx.getGlobalNiceConfig();
631
+ const qsModelConfig = await ctx.getModelConfig();
632
+ const qsModel = mode === 'opencode' ? openCodeConfig?.model : mode !== 'shell' ? qsModelConfig?.defaultModel : undefined;
633
+ const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
634
+ const session = new Session({
635
+ workingDir: casePath,
636
+ mux: ctx.mux,
637
+ useMux: true,
638
+ mode: mode,
639
+ niceConfig: niceConfig,
640
+ model: qsModel,
641
+ claudeMode: qsClaudeModeConfig.claudeMode,
642
+ allowedTools: qsClaudeModeConfig.allowedTools,
643
+ openCodeConfig: mode === 'opencode' ? openCodeConfig : undefined,
644
+ });
645
+ // Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
646
+ // so the initial state already has the phrase configured (only if globally enabled)
647
+ if (mode === 'claude' && ctx.store.getConfig().ralphEnabled) {
648
+ autoConfigureRalph(session, casePath, ctx);
649
+ if (!session.ralphTracker.enabled) {
650
+ session.ralphTracker.enable();
651
+ session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
652
+ }
653
+ }
654
+ ctx.addSession(session);
655
+ ctx.store.incrementSessionsCreated();
656
+ ctx.persistSessionState(session);
657
+ await ctx.setupSessionListeners(session);
658
+ getLifecycleLog().log({
659
+ event: 'created',
660
+ sessionId: session.id,
661
+ name: session.name,
662
+ reason: 'quick_start',
663
+ });
664
+ ctx.broadcast('session:created', ctx.getSessionStateWithRespawn(session));
665
+ // Start in the appropriate mode
666
+ try {
667
+ if (mode === 'shell') {
668
+ await session.startShell();
669
+ getLifecycleLog().log({
670
+ event: 'started',
671
+ sessionId: session.id,
672
+ name: session.name,
673
+ mode: 'shell',
674
+ });
675
+ ctx.broadcast('session:interactive', { id: session.id, mode: 'shell' });
676
+ }
677
+ else {
678
+ // Both 'claude' and 'opencode' modes use startInteractive()
679
+ await session.startInteractive();
680
+ getLifecycleLog().log({
681
+ event: 'started',
682
+ sessionId: session.id,
683
+ name: session.name,
684
+ mode,
685
+ });
686
+ ctx.broadcast('session:interactive', { id: session.id, mode });
687
+ }
688
+ ctx.broadcast('session:updated', { session: ctx.getSessionStateWithRespawn(session) });
689
+ // Save lastUsedCase to settings for TUI/web sync
690
+ try {
691
+ const settingsFilePath = SETTINGS_PATH;
692
+ let settings = {};
693
+ try {
694
+ settings = JSON.parse(await fs.readFile(settingsFilePath, 'utf-8'));
695
+ }
696
+ catch (err) {
697
+ if (err.code !== 'ENOENT')
698
+ throw err;
699
+ }
700
+ settings.lastUsedCase = caseName;
701
+ const dir = dirname(settingsFilePath);
702
+ if (!existsSync(dir)) {
703
+ mkdirSync(dir, { recursive: true });
704
+ }
705
+ // Use async write to avoid blocking event loop
706
+ fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2)).catch((err) => {
707
+ // Non-critical but log for debugging
708
+ console.warn('[Server] Failed to save settings (lastUsedCase):', err);
709
+ });
710
+ }
711
+ catch (err) {
712
+ // Non-critical but log for debugging
713
+ console.warn('[Server] Failed to prepare settings update:', err);
714
+ }
715
+ return {
716
+ success: true,
717
+ sessionId: session.id,
718
+ casePath,
719
+ caseName,
720
+ };
721
+ }
722
+ catch (err) {
723
+ // Clean up session on error to prevent orphaned resources
724
+ await ctx.cleanupSession(session.id, true, 'quick_start_error');
725
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
726
+ }
727
+ });
728
+ }
729
+ //# sourceMappingURL=session-routes.js.map