aicodeman 1.45.0 → 1.47.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 (312) hide show
  1. package/README.md +26 -6
  2. package/dist/agent-codex-identity.d.ts +26 -0
  3. package/dist/agent-codex-identity.d.ts.map +1 -0
  4. package/dist/agent-codex-identity.js +253 -0
  5. package/dist/agent-codex-identity.js.map +1 -0
  6. package/dist/agent-inspection.d.ts +32 -0
  7. package/dist/agent-inspection.d.ts.map +1 -0
  8. package/dist/agent-inspection.js +196 -0
  9. package/dist/agent-inspection.js.map +1 -0
  10. package/dist/agent-staging.d.ts +130 -0
  11. package/dist/agent-staging.d.ts.map +1 -0
  12. package/dist/agent-staging.js +312 -0
  13. package/dist/agent-staging.js.map +1 -0
  14. package/dist/cli-agent.d.ts +31 -2
  15. package/dist/cli-agent.d.ts.map +1 -1
  16. package/dist/cli-agent.js +279 -34
  17. package/dist/cli-agent.js.map +1 -1
  18. package/dist/cli-history.d.ts +20 -0
  19. package/dist/cli-history.d.ts.map +1 -0
  20. package/dist/cli-history.js +61 -0
  21. package/dist/cli-history.js.map +1 -0
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +6 -1
  24. package/dist/cli.js.map +1 -1
  25. package/dist/codex-rollout-format.d.ts +66 -0
  26. package/dist/codex-rollout-format.d.ts.map +1 -0
  27. package/dist/codex-rollout-format.js +111 -0
  28. package/dist/codex-rollout-format.js.map +1 -0
  29. package/dist/codex-transcript.d.ts +3 -0
  30. package/dist/codex-transcript.d.ts.map +1 -1
  31. package/dist/codex-transcript.js +13 -41
  32. package/dist/codex-transcript.js.map +1 -1
  33. package/dist/config/cli-registry/schema.d.ts +12 -7
  34. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  35. package/dist/config/cli-registry/schema.js +26 -0
  36. package/dist/config/cli-registry/schema.js.map +1 -1
  37. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  38. package/dist/config/cli-registry/stock.js +40 -1
  39. package/dist/config/cli-registry/stock.js.map +1 -1
  40. package/dist/config/cli-registry/types.d.ts +57 -0
  41. package/dist/config/cli-registry/types.d.ts.map +1 -1
  42. package/dist/config/server-timing.d.ts +6 -0
  43. package/dist/config/server-timing.d.ts.map +1 -1
  44. package/dist/config/server-timing.js +9 -0
  45. package/dist/config/server-timing.js.map +1 -1
  46. package/dist/daemon-control.d.ts +1 -0
  47. package/dist/daemon-control.d.ts.map +1 -1
  48. package/dist/daemon-control.js +2 -0
  49. package/dist/daemon-control.js.map +1 -1
  50. package/dist/deepseek-transcript.d.ts +9 -7
  51. package/dist/deepseek-transcript.d.ts.map +1 -1
  52. package/dist/deepseek-transcript.js +9 -7
  53. package/dist/deepseek-transcript.js.map +1 -1
  54. package/dist/docker-hosts.d.ts +8 -0
  55. package/dist/docker-hosts.d.ts.map +1 -1
  56. package/dist/docker-hosts.js +29 -0
  57. package/dist/docker-hosts.js.map +1 -1
  58. package/dist/git-fetch-scheduler.d.ts +124 -0
  59. package/dist/git-fetch-scheduler.d.ts.map +1 -0
  60. package/dist/git-fetch-scheduler.js +249 -0
  61. package/dist/git-fetch-scheduler.js.map +1 -0
  62. package/dist/git-fetch.d.ts +241 -0
  63. package/dist/git-fetch.d.ts.map +1 -0
  64. package/dist/git-fetch.js +392 -0
  65. package/dist/git-fetch.js.map +1 -0
  66. package/dist/git-workspace-status.d.ts +36 -1
  67. package/dist/git-workspace-status.d.ts.map +1 -1
  68. package/dist/git-workspace-status.js +42 -5
  69. package/dist/git-workspace-status.js.map +1 -1
  70. package/dist/hooks-config.d.ts +14 -2
  71. package/dist/hooks-config.d.ts.map +1 -1
  72. package/dist/hooks-config.js +39 -3
  73. package/dist/hooks-config.js.map +1 -1
  74. package/dist/local-history.d.ts +71 -0
  75. package/dist/local-history.d.ts.map +1 -0
  76. package/dist/local-history.js +750 -0
  77. package/dist/local-history.js.map +1 -0
  78. package/dist/mcp-server.d.ts.map +1 -1
  79. package/dist/mcp-server.js +2 -2
  80. package/dist/mcp-server.js.map +1 -1
  81. package/dist/mux-interface.d.ts +5 -2
  82. package/dist/mux-interface.d.ts.map +1 -1
  83. package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
  84. package/dist/prompt-cache-keepwarm.js +5 -3
  85. package/dist/prompt-cache-keepwarm.js.map +1 -1
  86. package/dist/ralph-fix-plan-watcher.d.ts +13 -1
  87. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -1
  88. package/dist/ralph-fix-plan-watcher.js +21 -3
  89. package/dist/ralph-fix-plan-watcher.js.map +1 -1
  90. package/dist/ralph-tracker.d.ts +6 -0
  91. package/dist/ralph-tracker.d.ts.map +1 -1
  92. package/dist/ralph-tracker.js +8 -0
  93. package/dist/ralph-tracker.js.map +1 -1
  94. package/dist/session.d.ts +113 -0
  95. package/dist/session.d.ts.map +1 -1
  96. package/dist/session.js +341 -13
  97. package/dist/session.js.map +1 -1
  98. package/dist/terminal-input-classifier.d.ts +8 -0
  99. package/dist/terminal-input-classifier.d.ts.map +1 -0
  100. package/dist/terminal-input-classifier.js +60 -0
  101. package/dist/terminal-input-classifier.js.map +1 -0
  102. package/dist/tmux-manager.d.ts +23 -1
  103. package/dist/tmux-manager.d.ts.map +1 -1
  104. package/dist/tmux-manager.js +54 -6
  105. package/dist/tmux-manager.js.map +1 -1
  106. package/dist/types/session.d.ts +15 -0
  107. package/dist/types/session.d.ts.map +1 -1
  108. package/dist/types/session.js.map +1 -1
  109. package/dist/web/network-auth-policy.d.ts +13 -3
  110. package/dist/web/network-auth-policy.d.ts.map +1 -1
  111. package/dist/web/network-auth-policy.js +15 -3
  112. package/dist/web/network-auth-policy.js.map +1 -1
  113. package/dist/web/ports/infra-port.d.ts +2 -0
  114. package/dist/web/ports/infra-port.d.ts.map +1 -1
  115. package/dist/web/public/admin-ui.js.gz +0 -0
  116. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  117. package/dist/web/public/app.fe8a6b4c.js +43 -0
  118. package/dist/web/public/app.fe8a6b4c.js.br +0 -0
  119. package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
  120. package/dist/web/public/approvals-ui.js.gz +0 -0
  121. package/dist/web/public/{constants.0b3cdde9.js → constants.affcdde6.js} +611 -12
  122. package/dist/web/public/constants.affcdde6.js.br +0 -0
  123. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  124. package/dist/web/public/cron-ui.js.gz +0 -0
  125. package/dist/web/public/entrance-animations.js.gz +0 -0
  126. package/dist/web/public/git-status-ui.js +88 -5
  127. package/dist/web/public/git-status-ui.js.br +0 -0
  128. package/dist/web/public/git-status-ui.js.gz +0 -0
  129. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  130. package/dist/web/public/home-sessions.js.gz +0 -0
  131. package/dist/web/public/host-wake-ui.js.gz +0 -0
  132. package/dist/web/public/i18n.a618ef4e.js +2 -0
  133. package/dist/web/public/i18n.a618ef4e.js.br +0 -0
  134. package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
  135. package/dist/web/public/{image-input.941386e7.js → image-input.a5d0f8f3.js} +18 -9
  136. package/dist/web/public/image-input.a5d0f8f3.js.br +0 -0
  137. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  138. package/dist/web/public/index.html +110 -19
  139. package/dist/web/public/index.html.br +0 -0
  140. package/dist/web/public/index.html.gz +0 -0
  141. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  142. package/dist/web/public/{keyboard-accessory.4e8b20ac.js → keyboard-accessory.ff64fafc.js} +5 -1
  143. package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
  144. package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
  145. package/dist/web/public/{mobile-handlers.1c0cbcd8.js → mobile-handlers.f7c3f692.js} +22 -0
  146. package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
  147. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  148. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  149. package/dist/web/public/mobile-overview.js.gz +0 -0
  150. package/dist/web/public/mobile.1bbd2d67.css +1 -0
  151. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  152. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  153. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  154. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  155. package/dist/web/public/{panels-ui.78c3defb.js → panels-ui.4a56ee63.js} +1 -1
  156. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  157. package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
  158. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  159. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  160. package/dist/web/public/readmymind-ui.js.gz +0 -0
  161. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  162. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  163. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  164. package/dist/web/public/session-lineage.js.gz +0 -0
  165. package/dist/web/public/{session-ui.ea6b1994.js → session-ui.426fb671.js} +8 -8
  166. package/dist/web/public/session-ui.426fb671.js.br +0 -0
  167. package/dist/web/public/session-ui.426fb671.js.gz +0 -0
  168. package/dist/web/public/{settings-ui.085dd202.js → settings-ui.a191154e.js} +11 -11
  169. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  170. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  171. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  172. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  173. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  174. package/dist/web/public/styles.4f0c67a0.css +1 -0
  175. package/dist/web/public/styles.4f0c67a0.css.br +0 -0
  176. package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
  177. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  178. package/dist/web/public/sw.js +2 -2
  179. package/dist/web/public/sw.js.br +0 -0
  180. package/dist/web/public/sw.js.gz +0 -0
  181. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  182. package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
  183. package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
  184. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  185. package/dist/web/public/terminal-keycode229-recovery.98843069.js +1 -0
  186. package/dist/web/public/terminal-keycode229-recovery.98843069.js.br +0 -0
  187. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  188. package/dist/web/public/terminal-split.js.gz +0 -0
  189. package/dist/web/public/terminal-tile.js +10 -6
  190. package/dist/web/public/terminal-tile.js.br +0 -0
  191. package/dist/web/public/terminal-tile.js.gz +0 -0
  192. package/dist/web/public/terminal-ui.7e787829.js +4 -0
  193. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  194. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
  195. package/dist/web/public/tile-grid.js.gz +0 -0
  196. package/dist/web/public/ultracode-panel.js.gz +0 -0
  197. package/dist/web/public/ultracode-windows.js.gz +0 -0
  198. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  199. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  200. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  201. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  202. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  203. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  204. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  205. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  206. package/dist/web/public/vendor/{xterm-predictive-echo.ae5e3d5d.js → xterm-predictive-echo.276b11d9.js} +1 -1
  207. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
  208. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  209. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
  210. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
  211. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm.css.gz +0 -0
  213. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  214. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  215. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  216. package/dist/web/public/webview-tabs.js.gz +0 -0
  217. package/dist/web/public/whats-new.json +8 -9
  218. package/dist/web/request-body-parser.d.ts +23 -0
  219. package/dist/web/request-body-parser.d.ts.map +1 -0
  220. package/dist/web/request-body-parser.js +57 -0
  221. package/dist/web/request-body-parser.js.map +1 -0
  222. package/dist/web/routes/agent-session-routes.d.ts +20 -0
  223. package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
  224. package/dist/web/routes/agent-session-routes.js +160 -0
  225. package/dist/web/routes/agent-session-routes.js.map +1 -0
  226. package/dist/web/routes/case-routes.d.ts.map +1 -1
  227. package/dist/web/routes/case-routes.js +32 -26
  228. package/dist/web/routes/case-routes.js.map +1 -1
  229. package/dist/web/routes/config-backup-routes.js +2 -2
  230. package/dist/web/routes/config-backup-routes.js.map +1 -1
  231. package/dist/web/routes/file-routes.d.ts +5 -0
  232. package/dist/web/routes/file-routes.d.ts.map +1 -1
  233. package/dist/web/routes/file-routes.js +5 -1
  234. package/dist/web/routes/file-routes.js.map +1 -1
  235. package/dist/web/routes/git-status-routes.d.ts +21 -7
  236. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  237. package/dist/web/routes/git-status-routes.js +78 -12
  238. package/dist/web/routes/git-status-routes.js.map +1 -1
  239. package/dist/web/routes/index.d.ts +1 -0
  240. package/dist/web/routes/index.d.ts.map +1 -1
  241. package/dist/web/routes/index.js +1 -0
  242. package/dist/web/routes/index.js.map +1 -1
  243. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  244. package/dist/web/routes/ralph-routes.js +52 -18
  245. package/dist/web/routes/ralph-routes.js.map +1 -1
  246. package/dist/web/routes/session-routes.d.ts.map +1 -1
  247. package/dist/web/routes/session-routes.js +101 -28
  248. package/dist/web/routes/session-routes.js.map +1 -1
  249. package/dist/web/routes/system-routes.d.ts.map +1 -1
  250. package/dist/web/routes/system-routes.js +87 -59
  251. package/dist/web/routes/system-routes.js.map +1 -1
  252. package/dist/web/routes/ws-routes.d.ts +5 -0
  253. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  254. package/dist/web/routes/ws-routes.js +84 -47
  255. package/dist/web/routes/ws-routes.js.map +1 -1
  256. package/dist/web/schemas.d.ts +30 -7
  257. package/dist/web/schemas.d.ts.map +1 -1
  258. package/dist/web/schemas.js +40 -1
  259. package/dist/web/schemas.js.map +1 -1
  260. package/dist/web/server.d.ts +5 -2
  261. package/dist/web/server.d.ts.map +1 -1
  262. package/dist/web/server.js +102 -18
  263. package/dist/web/server.js.map +1 -1
  264. package/dist/web/settings-writer.d.ts +47 -0
  265. package/dist/web/settings-writer.d.ts.map +1 -0
  266. package/dist/web/settings-writer.js +110 -0
  267. package/dist/web/settings-writer.js.map +1 -0
  268. package/package.json +1 -1
  269. package/skills/codeman/SKILL.md +25 -6
  270. package/skills/codeman/reference/history.md +84 -0
  271. package/skills/codeman/reference/inspection-and-staging.md +152 -0
  272. package/skills/codeman/reference/messaging.md +1 -1
  273. package/skills/codeman/reference/verbs.md +18 -3
  274. package/dist/web/public/app.42d0abae.js +0 -43
  275. package/dist/web/public/app.42d0abae.js.br +0 -0
  276. package/dist/web/public/app.42d0abae.js.gz +0 -0
  277. package/dist/web/public/constants.0b3cdde9.js.br +0 -0
  278. package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
  279. package/dist/web/public/i18n.1e68220d.js +0 -2
  280. package/dist/web/public/i18n.1e68220d.js.br +0 -0
  281. package/dist/web/public/i18n.1e68220d.js.gz +0 -0
  282. package/dist/web/public/image-input.941386e7.js.br +0 -0
  283. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  284. package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
  285. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  286. package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
  287. package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
  288. package/dist/web/public/mobile.12fb18de.css +0 -1
  289. package/dist/web/public/mobile.12fb18de.css.br +0 -0
  290. package/dist/web/public/mobile.12fb18de.css.gz +0 -0
  291. package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
  292. package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
  293. package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
  294. package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
  295. package/dist/web/public/settings-ui.085dd202.js.br +0 -0
  296. package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
  297. package/dist/web/public/styles.3b59b41d.css +0 -1
  298. package/dist/web/public/styles.3b59b41d.css.br +0 -0
  299. package/dist/web/public/styles.3b59b41d.css.gz +0 -0
  300. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  301. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  302. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +0 -1
  303. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  304. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  305. package/dist/web/public/terminal-ui.fb2a27f5.js +0 -2
  306. package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
  307. package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
  308. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +0 -0
  309. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  310. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +0 -4
  311. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  312. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
@@ -1,13 +1,12 @@
1
1
  {
2
- "version": "1.45.0",
2
+ "version": "1.47.0",
3
3
  "highlights": [
4
- "Rename a case and its folder (#680)",
5
- "A per-session agent inbox (#690)",
6
- "Pick the agent from Cmd+K (#685)",
7
- "A resizable header toolbar (#689)",
8
- "GPU and CPU clock readings (#692, opt-in)",
9
- "A tunnel no longer locks everyone out after ten typos (#684, multi-user mode)",
10
- "The first wheel notch moves a Codex transcript (#699)",
11
- "The wheel no longer moves a Codex menu's selection (#698)"
4
+ "The wrapped tab strip uses the room under the toolbar (#721)",
5
+ "Bodyless requests work through Cloudflare Tunnel (#725)",
6
+ "A desktop dead key is typed once, and Ctrl+V stays a paste after a replay (#726, #716)",
7
+ "Remote sessions no longer touch the local disk at their path (#722, #727, #728)",
8
+ "Rendered Markdown fills a resized File Viewer (#730)",
9
+ "A middle click on a file path opens the file (#731)",
10
+ "No more duplicate-key warning on build (#729)"
12
11
  ]
13
12
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview Accept empty, untyped API POSTs even when a reverse proxy frames them as chunked.
3
+ *
4
+ * Cloudflare Tunnel (cloudflared, Go's HTTP client) forwards a bodyless POST as an HTTP/1.1
5
+ * chunked request with no Content-Type and a zero-length body. Fastify only skips its
6
+ * content-type check for a `Content-Length: 0` request, so this framing was refused 415
7
+ * (`FST_ERR_CTP_INVALID_MEDIA_TYPE`) before the route ran, breaking actions such as opening a
8
+ * saved web tab. This parser lets such a request reach its route with `request.body === undefined`,
9
+ * exactly as a `Content-Length: 0` request does.
10
+ *
11
+ * Scope is POST under `/api/` only: Go frames an empty body of unknown length as chunked for
12
+ * POST, PUT and PATCH and drops it for DELETE, and every PUT/PATCH the frontend sends carries a
13
+ * JSON body. Anything else (another method, a non-API route, a Content-Type, a non-zero
14
+ * Content-Length, or a single body byte) is still the 415 Fastify would have answered.
15
+ *
16
+ * ⚠️ It owns the ROOT `'*'` content-type slot. The JSON, `text/plain` (kept RAW, see the CSRF
17
+ * note in CLAUDE.md) and multipart parsers match first; the web-tab proxy registers its own
18
+ * `'*'` in an encapsulated scope and is unaffected. Fastify refuses a second root `'*'` parser at
19
+ * boot (`FST_ERR_CTP_ALREADY_PRESENT`), so extend this function instead of adding one.
20
+ */
21
+ import { type FastifyInstance } from 'fastify';
22
+ export declare function installEmptyApiPostParser(app: FastifyInstance): void;
23
+ //# sourceMappingURL=request-body-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-body-parser.d.ts","sourceRoot":"","sources":["../../src/web/request-body-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAc,KAAK,eAAe,EAAuB,MAAM,SAAS,CAAC;AAGhF,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAmCpE"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Accept empty, untyped API POSTs even when a reverse proxy frames them as chunked.
3
+ *
4
+ * Cloudflare Tunnel (cloudflared, Go's HTTP client) forwards a bodyless POST as an HTTP/1.1
5
+ * chunked request with no Content-Type and a zero-length body. Fastify only skips its
6
+ * content-type check for a `Content-Length: 0` request, so this framing was refused 415
7
+ * (`FST_ERR_CTP_INVALID_MEDIA_TYPE`) before the route ran, breaking actions such as opening a
8
+ * saved web tab. This parser lets such a request reach its route with `request.body === undefined`,
9
+ * exactly as a `Content-Length: 0` request does.
10
+ *
11
+ * Scope is POST under `/api/` only: Go frames an empty body of unknown length as chunked for
12
+ * POST, PUT and PATCH and drops it for DELETE, and every PUT/PATCH the frontend sends carries a
13
+ * JSON body. Anything else (another method, a non-API route, a Content-Type, a non-zero
14
+ * Content-Length, or a single body byte) is still the 415 Fastify would have answered.
15
+ *
16
+ * ⚠️ It owns the ROOT `'*'` content-type slot. The JSON, `text/plain` (kept RAW, see the CSRF
17
+ * note in CLAUDE.md) and multipart parsers match first; the web-tab proxy registers its own
18
+ * `'*'` in an encapsulated scope and is unaffected. Fastify refuses a second root `'*'` parser at
19
+ * boot (`FST_ERR_CTP_ALREADY_PRESENT`), so extend this function instead of adding one.
20
+ */
21
+ import { errorCodes } from 'fastify';
22
+ export function installEmptyApiPostParser(app) {
23
+ // Existing JSON, text and multipart parsers take precedence. The webview
24
+ // proxy installs its own parser in an encapsulated scope.
25
+ app.addContentTypeParser('*', async (req, payload) => {
26
+ // Fastify normally skips an unsupported parser on its 404 path. Preserve
27
+ // that behavior, including the unread stream used by referer fallbacks.
28
+ if (req.is404)
29
+ return undefined;
30
+ if (req.method !== 'POST' ||
31
+ !req.routeOptions.url?.startsWith('/api/') ||
32
+ req.headers['content-type'] !== undefined ||
33
+ (req.headers['content-length'] !== undefined && req.headers['content-length'] !== '0')) {
34
+ throw new errorCodes.FST_ERR_CTP_INVALID_MEDIA_TYPE();
35
+ }
36
+ // Wait for the terminating chunk to prove the body is empty. Reject on the
37
+ // first byte instead of buffering an unsupported body. Leave the socket
38
+ // alive so Fastify can send its 415 and close the connection on parser errors.
39
+ try {
40
+ for await (const chunk of payload.iterator({ destroyOnReturn: false })) {
41
+ if (Buffer.byteLength(chunk) > 0)
42
+ throw new errorCodes.FST_ERR_CTP_INVALID_MEDIA_TYPE();
43
+ }
44
+ }
45
+ catch (error) {
46
+ // Match Fastify's body reader: an interrupted request is a client error.
47
+ const err = error;
48
+ if (typeof err.statusCode !== 'number' || err.statusCode < 400)
49
+ err.statusCode = 400;
50
+ throw err;
51
+ }
52
+ // No synthetic JSON or header rewriting: required-body validation still
53
+ // sees the same undefined value as a direct Content-Length: 0 request.
54
+ return undefined;
55
+ });
56
+ }
57
+ //# sourceMappingURL=request-body-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-body-parser.js","sourceRoot":"","sources":["../../src/web/request-body-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAA6C,MAAM,SAAS,CAAC;AAGhF,MAAM,UAAU,yBAAyB,CAAC,GAAoB;IAC5D,yEAAyE;IACzE,0DAA0D;IAC1D,GAAG,CAAC,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,GAAmB,EAAE,OAAiB,EAAE,EAAE;QAC7E,yEAAyE;QACzE,wEAAwE;QACxE,IAAI,GAAG,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAEhC,IACE,GAAG,CAAC,MAAM,KAAK,MAAM;YACrB,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC;YAC1C,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,KAAK,SAAS;YACzC,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,GAAG,CAAC,EACtF,CAAC;YACD,MAAM,IAAI,UAAU,CAAC,8BAA8B,EAAE,CAAC;QACxD,CAAC;QAED,2EAA2E;QAC3E,wEAAwE;QACxE,+EAA+E;QAC/E,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;gBACvE,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,EAAE,CAAC;YAC1F,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yEAAyE;YACzE,MAAM,GAAG,GAAG,KAAwC,CAAC;YACrD,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG;gBAAE,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;YACrF,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,wEAAwE;QACxE,uEAAuE;QACvE,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Agent-facing bounded inspection and guarded stage/submit endpoints.
3
+ * Ownership is checked before any disk or pane read. Native evidence comes only
4
+ * from strict conversation attribution; all other modes get labeled snapshots.
5
+ *
6
+ * Experimental and opt-in: all three routes answer `403 FORBIDDEN`, naming the
7
+ * setting, until the synced `agentCoordinationEnabled` is on (default OFF), the
8
+ * same shape as `cliManagementEnabled` and `mcpSyncEnabled`. The gate runs first,
9
+ * before session lookup, so a disabled install reveals nothing about its sessions.
10
+ */
11
+ import type { FastifyInstance } from 'fastify';
12
+ import type { SessionPort, InfraPort } from '../ports/index.js';
13
+ /** Default OFF, same shape as `readMcpSyncEnabled`: read fresh so a toggle applies at once. */
14
+ export declare function readAgentCoordinationEnabled(): Promise<boolean>;
15
+ /** The refusal every route sends while the setting is off. Exported for the tests. */
16
+ export declare const AGENT_COORDINATION_DISABLED_MESSAGE = "Experimental agent coordination (inspect, stage, submit) is disabled. Turn on \"Experimental agent coordination\" in Settings (agentCoordinationEnabled) and save first.";
17
+ /** A Node error code (`EACCES`, `EIO`, ...) or the error's class name; never its message. */
18
+ export declare function errorCode(error: unknown): string;
19
+ export declare function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPort & InfraPort): void;
20
+ //# sourceMappingURL=agent-session-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/agent-session-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAehE,+FAA+F;AAC/F,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,OAAO,CAAC,CAGrE;AAED,sFAAsF;AACtF,eAAO,MAAM,mCAAmC,6KAC0H,CAAC;AAO3K,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CA+InG"}
@@ -0,0 +1,160 @@
1
+ import { getCli } from '../../config/cli-registry/registry.js';
2
+ import { findExactCodexConversation, resolveEffectiveCodexHome } from '../../agent-codex-identity.js';
3
+ import { InspectionReader, InspectionConflict, boundedUtf8Tail } from '../../agent-inspection.js';
4
+ import { GuardedStageStore, GuardedInputConflict, guardedComposerChrome, MAY_BE_TYPED, } from '../../agent-staging.js';
5
+ import { ApiErrorCode, createErrorResponse } from '../../types.js';
6
+ import { findSessionOrFail, getAuthUser, parseBody, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
7
+ import { AgentInspectQuerySchema, AgentStageSchema, AgentSubmitSchema } from '../schemas.js';
8
+ /** Default OFF, same shape as `readMcpSyncEnabled`: read fresh so a toggle applies at once. */
9
+ export async function readAgentCoordinationEnabled() {
10
+ const settings = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
11
+ return settings.agentCoordinationEnabled === true;
12
+ }
13
+ /** The refusal every route sends while the setting is off. Exported for the tests. */
14
+ export const AGENT_COORDINATION_DISABLED_MESSAGE = 'Experimental agent coordination (inspect, stage, submit) is disabled. Turn on "Experimental agent coordination" in Settings (agentCoordinationEnabled) and save first.';
15
+ async function gate() {
16
+ if (await readAgentCoordinationEnabled())
17
+ return null;
18
+ return createErrorResponse(ApiErrorCode.FORBIDDEN, AGENT_COORDINATION_DISABLED_MESSAGE);
19
+ }
20
+ /** A Node error code (`EACCES`, `EIO`, ...) or the error's class name; never its message. */
21
+ export function errorCode(error) {
22
+ const code = error?.code;
23
+ if (typeof code === 'string' && /^[A-Z0-9_]{1,40}$/.test(code))
24
+ return code;
25
+ return error instanceof Error ? error.constructor.name : 'unknown';
26
+ }
27
+ export function registerAgentSessionRoutes(app, ctx) {
28
+ const reader = new InspectionReader();
29
+ const stages = new GuardedStageStore();
30
+ app.addHook('onClose', async () => {
31
+ reader.clear();
32
+ stages.clear();
33
+ });
34
+ const generation = (session) => session.inspectionGeneration;
35
+ const target = (session) => ({
36
+ id: session.id,
37
+ chrome: () => guardedComposerChrome(getCli(session.mode)),
38
+ snapshot: () => {
39
+ if (ctx.sessions.get(session.id) !== session)
40
+ return null;
41
+ const snapshot = session.guardedInputSnapshot?.();
42
+ return snapshot ?? null;
43
+ },
44
+ write: (text) => session.stageGuardedInput(text),
45
+ submit: (expected) => session.submitGuardedInput(expected),
46
+ });
47
+ app.get('/api/sessions/:id/inspect', async (req, reply) => {
48
+ const refused = await gate();
49
+ if (refused)
50
+ return reply.code(403).send(refused);
51
+ const { id } = req.params;
52
+ const session = findSessionOrFail(ctx, id, req);
53
+ const { maxBytes, since } = parseBody(AgentInspectQuerySchema, req.query);
54
+ const initialGeneration = generation(session);
55
+ const identity = { sessionId: session.id, mode: session.mode };
56
+ const activity = { status: session.status, lastSubmitAt: session.lastSubmitAt || null, observedAt: Date.now() };
57
+ try {
58
+ if (getCli(session.mode)?.capabilities.transcript === 'codex-rollout' && !session.remote && !session.docker) {
59
+ const conversation = await findExactCodexConversation({
60
+ codexHome: resolveEffectiveCodexHome(session.getCodexHomeOverride(), session.workingDir),
61
+ sessionId: session.id,
62
+ workingDir: session.workingDir,
63
+ resumeSessionId: session.codexConfig?.resumeSessionId,
64
+ }, { maxEntries: 8192, maxBytes: 1048576 });
65
+ if (generation(session) !== initialGeneration || ctx.sessions.get(id) !== session)
66
+ throw new InspectionConflict('Session changed during inspection');
67
+ if (conversation) {
68
+ const result = await reader.read(conversation.path, {
69
+ owner: getAuthUser(req).username,
70
+ sessionId: id,
71
+ generation: initialGeneration,
72
+ conversationId: conversation.id,
73
+ }, maxBytes, since);
74
+ if (generation(session) !== initialGeneration || ctx.sessions.get(id) !== session)
75
+ throw new InspectionConflict('Session changed during inspection');
76
+ return {
77
+ success: true,
78
+ data: {
79
+ source: 'codex-jsonl',
80
+ identity: { ...identity, conversationId: conversation.id },
81
+ activity,
82
+ incremental: true,
83
+ maxBytes,
84
+ ...result,
85
+ },
86
+ };
87
+ }
88
+ }
89
+ if (since)
90
+ throw new InspectionConflict('Attributed transcript unavailable; this cursor cannot resume a pane snapshot');
91
+ const frame = session.muxName ? ctx.mux.capturePaneText?.(session.muxName) : null;
92
+ const evidence = frame ?? session.terminalBuffer;
93
+ return {
94
+ success: true,
95
+ data: {
96
+ source: 'pane-snapshot',
97
+ identity,
98
+ activity,
99
+ incremental: false,
100
+ cursor: null,
101
+ maxBytes,
102
+ evidence: boundedUtf8Tail(evidence, maxBytes),
103
+ truncated: evidence.length > maxBytes || Buffer.byteLength(evidence) > maxBytes,
104
+ },
105
+ };
106
+ }
107
+ catch (error) {
108
+ if (error instanceof InspectionConflict)
109
+ return reply.code(409).send(createErrorResponse(ApiErrorCode.CONFLICT, error.message));
110
+ // Anything else (an unreadable rollout, an IO error) is not a retryable cursor
111
+ // conflict. Log the session and the error code only: the message can carry the
112
+ // native transcript path, which must never reach the log or the response.
113
+ console.warn(`[agent-inspect] session ${session.id}: inspection failed (${errorCode(error)})`);
114
+ return reply.code(422).send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Inspection source unavailable'));
115
+ }
116
+ });
117
+ app.post('/api/sessions/:id/stage', async (req, reply) => {
118
+ const refused = await gate();
119
+ if (refused)
120
+ return reply.code(403).send(refused);
121
+ const session = findSessionOrFail(ctx, req.params.id, req);
122
+ const { message } = parseBody(AgentStageSchema, req.body);
123
+ try {
124
+ const receipt = await stages.stage(target(session), getAuthUser(req).username, message);
125
+ return { success: true, data: receipt };
126
+ }
127
+ catch (error) {
128
+ if (!(error instanceof GuardedInputConflict))
129
+ console.warn(`[agent-stage] session ${session.id}: delivery failed (${errorCode(error)})`);
130
+ return reply
131
+ .code(409)
132
+ .send(createErrorResponse(ApiErrorCode.CONFLICT, error instanceof GuardedInputConflict
133
+ ? error.message
134
+ : `Stage delivery failed or ambiguous; ${MAY_BE_TYPED}`));
135
+ }
136
+ });
137
+ app.post('/api/sessions/:id/submit', async (req, reply) => {
138
+ const refused = await gate();
139
+ if (refused)
140
+ return reply.code(403).send(refused);
141
+ const session = findSessionOrFail(ctx, req.params.id, req);
142
+ const { stageId, confirmSha } = parseBody(AgentSubmitSchema, req.body);
143
+ try {
144
+ return {
145
+ success: true,
146
+ data: await stages.submit(target(session), getAuthUser(req).username, stageId, confirmSha),
147
+ };
148
+ }
149
+ catch (error) {
150
+ if (!(error instanceof GuardedInputConflict))
151
+ console.warn(`[agent-submit] session ${session.id}: delivery failed (${errorCode(error)})`);
152
+ return reply
153
+ .code(409)
154
+ .send(createErrorResponse(ApiErrorCode.CONFLICT, error instanceof GuardedInputConflict
155
+ ? error.message
156
+ : 'Submit delivery failed or ambiguous; receipt consumed, inspect the pane'));
157
+ }
158
+ });
159
+ }
160
+ //# sourceMappingURL=agent-session-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-routes.js","sourceRoot":"","sources":["../../../src/web/routes/agent-session-routes.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"case-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/case-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA2E1C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA4D9C;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAE1E;AAgID,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GACxD,IAAI,CAo+DN"}
1
+ {"version":3,"file":"case-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/case-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAiF1C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA4D9C;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAE1E;AAgID,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GACxD,IAAI,CA+9DN"}
@@ -22,6 +22,7 @@ import { boundedPathExists, describeUnknownPath, probePath, probePathKind } from
22
22
  import { readAgentCaseMarker } from '../../agent-case-marker.js';
23
23
  import { settingsWriteBlocker, writeHooksConfig } from '../../hooks-config.js';
24
24
  import { canAccessOwned, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, requireAdmin, resolveCasesDir, SETTINGS_PATH, validatePathWithinBase, parseBody, readJsonConfig, } from '../route-helpers.js';
25
+ import { assertSettingsWritable, mutateSettingsFile, SettingsWriteRefusedError, settingsWriteErrorResponse, } from '../settings-writer.js';
25
26
  import { isMultiUserMode } from '../../config/multiuser.js';
26
27
  import { SseEvent } from '../sse-events.js';
27
28
  import { ownerLayoutKey } from '../../tab-layout-persistence.js';
@@ -1529,6 +1530,19 @@ export function registerCaseRoutes(app, ctx) {
1529
1530
  if (open.some((s) => !canAccessOwned(user, s.owner))) {
1530
1531
  return fail(403, ApiErrorCode.FORBIDDEN, "Another user's session has this case open");
1531
1532
  }
1533
+ // The admin's rename writes settings.json only after the sessions are ended and the folder moved, and
1534
+ // the writer refuses a file that is not a JSON object: check it by the writer's own rule now, so no
1535
+ // session is ended for a rename that cannot complete. Not between the starting check and the lock below.
1536
+ if (isAdmin(req)) {
1537
+ try {
1538
+ await assertSettingsWritable();
1539
+ }
1540
+ catch (err) {
1541
+ if (err instanceof SettingsWriteRefusedError)
1542
+ return fail(409, ApiErrorCode.CONFLICT, err.message);
1543
+ throw err;
1544
+ }
1545
+ }
1532
1546
  // A session being created in the folder is not in ctx.sessions yet, so it could be neither listed nor
1533
1547
  // stopped: it would start in the old path after the move. Checked in the same tick that takes the lock
1534
1548
  // below (nothing is awaited in between), and the Session constructor refuses the folder from then on.
@@ -1551,20 +1565,24 @@ export function registerCaseRoutes(app, ctx) {
1551
1565
  if (moveFolder)
1552
1566
  await moveCaseFolder(oldPath, newPath);
1553
1567
  const originalLinks = isLinked ? await readLinkedCases() : undefined;
1554
- // The settings.json content before this rename wrote it, set only once a write is attempted (an
1555
- // absent file reads as {}, which must not be written back as a file that never existed).
1556
- let originalSettings;
1568
+ // The settings.json values this rename replaced, set only once it wrote them. A failure puts back
1569
+ // these keys alone, so a settings save that landed in between is kept.
1570
+ let settingsUndo;
1557
1571
  try {
1558
1572
  // settings.json is server-wide and caseOrder/lastUsedCase name the admin's cases, so only the
1559
1573
  // admin's rename touches it (a non-admin's own rename must not rewrite someone else's list).
1560
1574
  // Written before linked-cases.json so a failure here leaves the registry untouched.
1561
1575
  if (isAdmin(req)) {
1562
- const existing = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
1563
- const order = renameInOrder(existing.caseOrder, name, newName);
1564
- const last = existing.lastUsedCase === name ? newName : undefined;
1565
- if (order || last) {
1566
- originalSettings = existing;
1567
- await fs.writeFile(SETTINGS_PATH, JSON.stringify({ ...existing, ...(order ? { caseOrder: order } : {}), ...(last ? { lastUsedCase: last } : {}) }, null, 2));
1576
+ const { before, after } = await mutateSettingsFile((existing) => {
1577
+ const order = renameInOrder(existing.caseOrder, name, newName);
1578
+ const last = existing.lastUsedCase === name ? newName : undefined;
1579
+ if (!order && !last)
1580
+ return undefined;
1581
+ return { ...existing, ...(order ? { caseOrder: order } : {}), ...(last ? { lastUsedCase: last } : {}) };
1582
+ });
1583
+ if (after) {
1584
+ const keys = ['caseOrder', 'lastUsedCase'].filter((k) => after[k] !== before[k]);
1585
+ settingsUndo = Object.fromEntries(keys.map((k) => [k, before[k]]));
1568
1586
  }
1569
1587
  }
1570
1588
  if (originalLinks) {
@@ -1583,9 +1601,9 @@ export function registerCaseRoutes(app, ctx) {
1583
1601
  catch (err) {
1584
1602
  // The folder moved but the registries did not follow: put settings.json and the folder back
1585
1603
  // rather than leave a case whose name and folder disagree.
1586
- if (originalSettings) {
1587
- await fs.writeFile(SETTINGS_PATH, JSON.stringify(originalSettings, null, 2)).catch(() => { });
1588
- }
1604
+ const undo = settingsUndo;
1605
+ if (undo)
1606
+ await mutateSettingsFile((current) => ({ ...current, ...undo })).catch(() => { });
1589
1607
  if (moveFolder) {
1590
1608
  const movedBack = await fs.rename(newPath, oldPath).then(() => true, () => false);
1591
1609
  if (!movedBack) {
@@ -1638,24 +1656,12 @@ export function registerCaseRoutes(app, ctx) {
1638
1656
  return;
1639
1657
  const { order } = parseBody(CaseOrderSchema, req.body, 'Invalid order data');
1640
1658
  try {
1641
- const dir = getDataDir();
1642
- if (!existsSync(dir)) {
1643
- mkdirSync(dir, { recursive: true });
1644
- }
1645
- let existing = {};
1646
- try {
1647
- existing = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
1648
- }
1649
- catch {
1650
- /* ignore */
1651
- }
1652
- const merged = { ...existing, caseOrder: order };
1653
- await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
1659
+ await mutateSettingsFile((current) => ({ ...current, caseOrder: order }));
1654
1660
  ctx.broadcast(SseEvent.CaseOrderChanged, { order });
1655
1661
  return { success: true, data: { order } };
1656
1662
  }
1657
1663
  catch (err) {
1658
- return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
1664
+ return settingsWriteErrorResponse(err);
1659
1665
  }
1660
1666
  });
1661
1667
  app.get('/api/cases/:name', async (req) => {