aicodeman 1.46.0 → 1.48.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 (291) hide show
  1. package/README.md +3 -0
  2. package/dist/cli-notify.d.ts +34 -0
  3. package/dist/cli-notify.d.ts.map +1 -0
  4. package/dist/cli-notify.js +100 -0
  5. package/dist/cli-notify.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +16 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/cli-registry/patterns.d.ts +1 -1
  10. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  11. package/dist/config/cli-registry/patterns.js +4 -0
  12. package/dist/config/cli-registry/patterns.js.map +1 -1
  13. package/dist/config/cli-registry/schema.d.ts +6 -0
  14. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  15. package/dist/config/cli-registry/schema.js +62 -2
  16. package/dist/config/cli-registry/schema.js.map +1 -1
  17. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  18. package/dist/config/cli-registry/stock.js +43 -26
  19. package/dist/config/cli-registry/stock.js.map +1 -1
  20. package/dist/config/cli-registry/types.d.ts +47 -17
  21. package/dist/config/cli-registry/types.d.ts.map +1 -1
  22. package/dist/config/path-probe.d.ts +6 -0
  23. package/dist/config/path-probe.d.ts.map +1 -1
  24. package/dist/config/path-probe.js +6 -0
  25. package/dist/config/path-probe.js.map +1 -1
  26. package/dist/config-backup.d.ts +1 -1
  27. package/dist/config-backup.d.ts.map +1 -1
  28. package/dist/config-backup.js +2 -0
  29. package/dist/config-backup.js.map +1 -1
  30. package/dist/custom-model-hosts.d.ts +32 -8
  31. package/dist/custom-model-hosts.d.ts.map +1 -1
  32. package/dist/custom-model-hosts.js +10 -0
  33. package/dist/custom-model-hosts.js.map +1 -1
  34. package/dist/custom-model-injection-apply.d.ts +19 -5
  35. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  36. package/dist/custom-model-injection-apply.js +88 -8
  37. package/dist/custom-model-injection-apply.js.map +1 -1
  38. package/dist/custom-model-injection.d.ts +6 -0
  39. package/dist/custom-model-injection.d.ts.map +1 -1
  40. package/dist/custom-model-injection.js +92 -10
  41. package/dist/custom-model-injection.js.map +1 -1
  42. package/dist/discord-bridge.d.ts +338 -0
  43. package/dist/discord-bridge.d.ts.map +1 -0
  44. package/dist/discord-bridge.js +1256 -0
  45. package/dist/discord-bridge.js.map +1 -0
  46. package/dist/edit-history-local-source.d.ts +83 -0
  47. package/dist/edit-history-local-source.d.ts.map +1 -0
  48. package/dist/edit-history-local-source.js +198 -0
  49. package/dist/edit-history-local-source.js.map +1 -0
  50. package/dist/edit-history-parse.d.ts +114 -0
  51. package/dist/edit-history-parse.d.ts.map +1 -0
  52. package/dist/edit-history-parse.js +312 -0
  53. package/dist/edit-history-parse.js.map +1 -0
  54. package/dist/edit-history-store.d.ts +209 -0
  55. package/dist/edit-history-store.d.ts.map +1 -0
  56. package/dist/edit-history-store.js +599 -0
  57. package/dist/edit-history-store.js.map +1 -0
  58. package/dist/git-clone.d.ts +6 -1
  59. package/dist/git-clone.d.ts.map +1 -1
  60. package/dist/git-clone.js +79 -14
  61. package/dist/git-clone.js.map +1 -1
  62. package/dist/image-watcher.js +4 -4
  63. package/dist/image-watcher.js.map +1 -1
  64. package/dist/mux-interface.d.ts +10 -1
  65. package/dist/mux-interface.d.ts.map +1 -1
  66. package/dist/ralph-fix-plan-watcher.d.ts +13 -1
  67. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -1
  68. package/dist/ralph-fix-plan-watcher.js +21 -3
  69. package/dist/ralph-fix-plan-watcher.js.map +1 -1
  70. package/dist/ralph-tracker.d.ts +6 -0
  71. package/dist/ralph-tracker.d.ts.map +1 -1
  72. package/dist/ralph-tracker.js +8 -0
  73. package/dist/ralph-tracker.js.map +1 -1
  74. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  75. package/dist/session-cli-registry-bridge.js +4 -0
  76. package/dist/session-cli-registry-bridge.js.map +1 -1
  77. package/dist/session.d.ts +70 -3
  78. package/dist/session.d.ts.map +1 -1
  79. package/dist/session.js +147 -4
  80. package/dist/session.js.map +1 -1
  81. package/dist/tmux-manager.d.ts +40 -0
  82. package/dist/tmux-manager.d.ts.map +1 -1
  83. package/dist/tmux-manager.js +62 -0
  84. package/dist/tmux-manager.js.map +1 -1
  85. package/dist/types/discord.d.ts +55 -0
  86. package/dist/types/discord.d.ts.map +1 -0
  87. package/dist/types/discord.js +18 -0
  88. package/dist/types/discord.js.map +1 -0
  89. package/dist/types/index.d.ts +2 -0
  90. package/dist/types/index.d.ts.map +1 -1
  91. package/dist/types/index.js +2 -0
  92. package/dist/types/index.js.map +1 -1
  93. package/dist/types/session.d.ts +13 -0
  94. package/dist/types/session.d.ts.map +1 -1
  95. package/dist/types/session.js.map +1 -1
  96. package/dist/web/case-path.d.ts +2 -0
  97. package/dist/web/case-path.d.ts.map +1 -1
  98. package/dist/web/case-path.js +3 -3
  99. package/dist/web/case-path.js.map +1 -1
  100. package/dist/web/edit-history-sync.d.ts +158 -0
  101. package/dist/web/edit-history-sync.d.ts.map +1 -0
  102. package/dist/web/edit-history-sync.js +445 -0
  103. package/dist/web/edit-history-sync.js.map +1 -0
  104. package/dist/web/paste-image-gc.d.ts +6 -0
  105. package/dist/web/paste-image-gc.d.ts.map +1 -1
  106. package/dist/web/paste-image-gc.js +6 -0
  107. package/dist/web/paste-image-gc.js.map +1 -1
  108. package/dist/web/ports/session-port.d.ts +13 -0
  109. package/dist/web/ports/session-port.d.ts.map +1 -1
  110. package/dist/web/public/admin-ui.js.gz +0 -0
  111. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  112. package/dist/web/public/{app.3093527f.js → app.963b5ba8.js} +18 -18
  113. package/dist/web/public/app.963b5ba8.js.br +0 -0
  114. package/dist/web/public/app.963b5ba8.js.gz +0 -0
  115. package/dist/web/public/approvals-ui.js.gz +0 -0
  116. package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
  117. package/dist/web/public/constants.782170e1.js.br +0 -0
  118. package/dist/web/public/constants.782170e1.js.gz +0 -0
  119. package/dist/web/public/cron-ui.js.gz +0 -0
  120. package/dist/web/public/edit-history-ui.js +630 -0
  121. package/dist/web/public/edit-history-ui.js.br +0 -0
  122. package/dist/web/public/edit-history-ui.js.gz +0 -0
  123. package/dist/web/public/entrance-animations.js.gz +0 -0
  124. package/dist/web/public/git-status-ui.js.gz +0 -0
  125. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  126. package/dist/web/public/home-sessions.js.gz +0 -0
  127. package/dist/web/public/host-wake-ui.js.gz +0 -0
  128. package/dist/web/public/i18n.33f930d7.js +2 -0
  129. package/dist/web/public/i18n.33f930d7.js.br +0 -0
  130. package/dist/web/public/i18n.33f930d7.js.gz +0 -0
  131. package/dist/web/public/{image-input.941386e7.js → image-input.a5d0f8f3.js} +18 -9
  132. package/dist/web/public/image-input.a5d0f8f3.js.br +0 -0
  133. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  134. package/dist/web/public/index.html +162 -16
  135. package/dist/web/public/index.html.br +0 -0
  136. package/dist/web/public/index.html.gz +0 -0
  137. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  138. package/dist/web/public/{keyboard-accessory.4e8b20ac.js → keyboard-accessory.d4ec5957.js} +77 -2
  139. package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
  140. package/dist/web/public/keyboard-accessory.d4ec5957.js.gz +0 -0
  141. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  142. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  143. package/dist/web/public/mobile-overview.js.gz +0 -0
  144. package/dist/web/public/mobile.5ef2ce85.css +1 -0
  145. package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
  146. package/dist/web/public/mobile.5ef2ce85.css.gz +0 -0
  147. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  148. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  149. package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.b100727d.js} +2 -2
  150. package/dist/web/public/panels-ui.b100727d.js.br +0 -0
  151. package/dist/web/public/{panels-ui.4a56ee63.js.gz → panels-ui.b100727d.js.gz} +0 -0
  152. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  153. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  154. package/dist/web/public/readmymind-ui.js.gz +0 -0
  155. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  156. package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
  157. package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
  158. package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
  159. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  160. package/dist/web/public/security-notices.json +1 -21
  161. package/dist/web/public/session-lineage.js.gz +0 -0
  162. package/dist/web/public/{session-ui.186f9419.js → session-ui.5ac82f2c.js} +30 -30
  163. package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
  164. package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
  165. package/dist/web/public/settings-ui.d1d4fb99.js +106 -0
  166. package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
  167. package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
  168. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  169. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  170. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  171. package/dist/web/public/styles.77dc538f.css +1 -0
  172. package/dist/web/public/styles.77dc538f.css.br +0 -0
  173. package/dist/web/public/styles.77dc538f.css.gz +0 -0
  174. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  175. package/dist/web/public/sw.js +2 -2
  176. package/dist/web/public/sw.js.br +0 -0
  177. package/dist/web/public/sw.js.gz +0 -0
  178. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  179. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  180. package/dist/web/public/terminal-keycode229-recovery.98843069.js +1 -0
  181. package/dist/web/public/terminal-keycode229-recovery.98843069.js.br +0 -0
  182. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  183. package/dist/web/public/terminal-split.js.gz +0 -0
  184. package/dist/web/public/terminal-tile.js +8 -6
  185. package/dist/web/public/terminal-tile.js.br +0 -0
  186. package/dist/web/public/terminal-tile.js.gz +0 -0
  187. package/dist/web/public/{terminal-ui.7e787829.js → terminal-ui.d7c75ce8.js} +3 -3
  188. package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
  189. package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
  190. package/dist/web/public/tile-grid.js.gz +0 -0
  191. package/dist/web/public/ultracode-panel.js.gz +0 -0
  192. package/dist/web/public/ultracode-windows.js.gz +0 -0
  193. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  194. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  195. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  196. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  197. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  198. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  199. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  200. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  201. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  202. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  203. package/dist/web/public/vendor/xterm.css.gz +0 -0
  204. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  205. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  206. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  207. package/dist/web/public/webview-tabs.js.gz +0 -0
  208. package/dist/web/public/whats-new.json +9 -9
  209. package/dist/web/request-body-parser.d.ts +23 -0
  210. package/dist/web/request-body-parser.d.ts.map +1 -0
  211. package/dist/web/request-body-parser.js +57 -0
  212. package/dist/web/request-body-parser.js.map +1 -0
  213. package/dist/web/routes/case-routes.d.ts.map +1 -1
  214. package/dist/web/routes/case-routes.js +104 -8
  215. package/dist/web/routes/case-routes.js.map +1 -1
  216. package/dist/web/routes/config-backup-routes.d.ts +5 -1
  217. package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
  218. package/dist/web/routes/config-backup-routes.js +4 -1
  219. package/dist/web/routes/config-backup-routes.js.map +1 -1
  220. package/dist/web/routes/custom-model-routes.d.ts +7 -2
  221. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  222. package/dist/web/routes/custom-model-routes.js +51 -6
  223. package/dist/web/routes/custom-model-routes.js.map +1 -1
  224. package/dist/web/routes/discord-routes.d.ts +25 -0
  225. package/dist/web/routes/discord-routes.d.ts.map +1 -0
  226. package/dist/web/routes/discord-routes.js +206 -0
  227. package/dist/web/routes/discord-routes.js.map +1 -0
  228. package/dist/web/routes/edit-history-routes.d.ts +43 -0
  229. package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
  230. package/dist/web/routes/edit-history-routes.js +106 -0
  231. package/dist/web/routes/edit-history-routes.js.map +1 -0
  232. package/dist/web/routes/file-routes.d.ts +5 -0
  233. package/dist/web/routes/file-routes.d.ts.map +1 -1
  234. package/dist/web/routes/file-routes.js +67 -3
  235. package/dist/web/routes/file-routes.js.map +1 -1
  236. package/dist/web/routes/index.d.ts +2 -0
  237. package/dist/web/routes/index.d.ts.map +1 -1
  238. package/dist/web/routes/index.js +2 -0
  239. package/dist/web/routes/index.js.map +1 -1
  240. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  241. package/dist/web/routes/ralph-routes.js +45 -2
  242. package/dist/web/routes/ralph-routes.js.map +1 -1
  243. package/dist/web/routes/session-routes.d.ts +17 -0
  244. package/dist/web/routes/session-routes.d.ts.map +1 -1
  245. package/dist/web/routes/session-routes.js +92 -37
  246. package/dist/web/routes/session-routes.js.map +1 -1
  247. package/dist/web/routes/system-routes.d.ts.map +1 -1
  248. package/dist/web/routes/system-routes.js +4 -2
  249. package/dist/web/routes/system-routes.js.map +1 -1
  250. package/dist/web/schemas.d.ts +68 -1
  251. package/dist/web/schemas.d.ts.map +1 -1
  252. package/dist/web/schemas.js +77 -1
  253. package/dist/web/schemas.js.map +1 -1
  254. package/dist/web/sensitive-path.d.ts.map +1 -1
  255. package/dist/web/sensitive-path.js +15 -0
  256. package/dist/web/sensitive-path.js.map +1 -1
  257. package/dist/web/server.d.ts +43 -0
  258. package/dist/web/server.d.ts.map +1 -1
  259. package/dist/web/server.js +237 -15
  260. package/dist/web/server.js.map +1 -1
  261. package/package.json +1 -1
  262. package/dist/web/public/app.3093527f.js.br +0 -0
  263. package/dist/web/public/app.3093527f.js.gz +0 -0
  264. package/dist/web/public/constants.affcdde6.js.br +0 -0
  265. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  266. package/dist/web/public/i18n.70265257.js +0 -2
  267. package/dist/web/public/i18n.70265257.js.br +0 -0
  268. package/dist/web/public/i18n.70265257.js.gz +0 -0
  269. package/dist/web/public/image-input.941386e7.js.br +0 -0
  270. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  271. package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
  272. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  273. package/dist/web/public/mobile.1bbd2d67.css +0 -1
  274. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  275. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  276. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  277. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  278. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  279. package/dist/web/public/session-ui.186f9419.js.br +0 -0
  280. package/dist/web/public/session-ui.186f9419.js.gz +0 -0
  281. package/dist/web/public/settings-ui.a191154e.js +0 -106
  282. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  283. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  284. package/dist/web/public/styles.7238eae8.css +0 -1
  285. package/dist/web/public/styles.7238eae8.css.br +0 -0
  286. package/dist/web/public/styles.7238eae8.css.gz +0 -0
  287. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +0 -1
  288. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  289. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  290. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  291. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
Binary file
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "1.46.0",
2
+ "version": "1.48.0",
3
3
  "highlights": [
4
- "Fetch from the Git status window (#714)",
5
- "Start a session beside any tab (#712, opt-in)",
6
- "The phone keyboard no longer costs Claude its transcript (#711)",
7
- "Typing stays smooth in a long prompt (#713)",
8
- "Links survive wrapping and spaces (#686, #687)",
9
- "`codeman agent spawn` in a project directory (#708)",
10
- "Read saved Codex conversations offline (#707)",
11
- "Experimental: bounded inspection and guarded submission for agents (#694, opt-in)"
4
+ "Talk to your agents from Discord (#741)",
5
+ "Edit history panel (#693)",
6
+ "Custom endpoints go further (#752, #755, #757)",
7
+ "Clone into a custom folder, and make a folder from the picker (#744, #745)",
8
+ "Relative links follow your `cd` (#740)",
9
+ "File links keep the whole name (#735)",
10
+ "A failed settings save says so (#736)",
11
+ "Phone keys show a press (#737)"
12
12
  ]
13
13
  }
@@ -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"}
@@ -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;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"}
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;AAmF1C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAgE9C;;;;;;;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,CA4kEN"}
@@ -15,6 +15,8 @@ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.
15
15
  import { CreateCaseSchema, CloneCaseSchema, ClonePreflightSchema, LinkCaseSchema, CaseOrderSchema, CaseDefaultGroupSchema, RenameCaseSchema, RemoteCaseLinkSchema, RemoteHostSchema, DockerCaseLinkSchema, DockerCaseAdoptSchema, DockerAdoptPreflightSchema, DockerBrowseSchema, DockerHostSchema, DockerExportSchema, DockerImportSchema, DockerQuickCreateSchema, } from '../schemas.js';
16
16
  import { exportDockerCase, importDockerBundle, listDockerExports, exportBundleName } from '../../docker-export.js';
17
17
  import { cloneRepository, isGitAvailable, isSafeGitRef, parseGitRepositoryUrl, probeGitRemote, } from '../../git-clone.js';
18
+ import { isPathWithin } from '../../git-worktrees.js';
19
+ import { defaultDockerWorkspaces } from './git-status-routes.js';
18
20
  import { generateClaudeMd } from '../../templates/claude-md.js';
19
21
  import { prepareNewCasePath } from '../case-path.js';
20
22
  import { CaseRenameError, isCaseSessionStarting, moveCaseFolder, moveClaudeHistory, renameInOrder, siblingPath, withCaseLock, } from '../../case-rename.js';
@@ -54,6 +56,8 @@ const APP_VERSION = (() => {
54
56
  * `/api/cases/link` is admin-only for).
55
57
  */
56
58
  const LOCAL_CLONE_ADMIN_ONLY = 'Cloning from a local path is admin-only in multi-user mode. Use a repository URL instead.';
59
+ /** Refusal text for a clone at or inside a Docker case workspace (git would run its hooks on the host). */
60
+ const DOCKER_CLONE_REFUSAL = 'Cloning is not available into a folder inside a Docker case workspace: a container can write there, and git would run the new repository’s hooks and filters on this machine. Clone outside the workspace and link it, or clone from inside the container.';
57
61
  /**
58
62
  * Whether a clone or preflight must run with git's credential helpers cleared:
59
63
  * a non-admin in multi-user mode. Every user's git runs as the one server
@@ -519,7 +523,8 @@ export function registerCaseRoutes(app, ctx) {
519
523
  * request died mid-clone still sees the case appear over SSE when git finishes.
520
524
  *
521
525
  * Deliberately NOT admin-gated in multi-user mode: unlike `/api/cases/link`,
522
- * this writes only inside the caller's own `resolveCasesDir`. Two things would
526
+ * this writes only inside the caller's own `resolveCasesDir`, EXCEPT with a `path` (a custom folder,
527
+ * registered as a linked case in the shared registry), which is admin-only like Link Existing. Two things would
523
528
  * otherwise read through that boundary: a `local`-transport source (refused for
524
529
  * non-admins) and the server account's git credential helpers, which every user
525
530
  * shares (cleared for non-admins, see `cloneWithoutCredentialHelpers`).
@@ -531,7 +536,7 @@ export function registerCaseRoutes(app, ctx) {
531
536
  * user's machine the moment a session starts in the case.
532
537
  */
533
538
  app.post('/api/cases/clone', async (req, reply) => {
534
- const { name, repository, ref, shallow, description } = parseBody(CloneCaseSchema, req.body);
539
+ const { name, repository, ref, shallow, description, path: customPath } = parseBody(CloneCaseSchema, req.body);
535
540
  const user = getAuthUser(req);
536
541
  const parsed = parseGitRepositoryUrl(repository);
537
542
  if (!parsed.cloneable)
@@ -546,8 +551,52 @@ export function registerCaseRoutes(app, ctx) {
546
551
  if (!isGitAvailable()) {
547
552
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'git is not installed on this machine (or not on the server’s PATH).');
548
553
  }
554
+ // A custom folder writes outside the cases directory and registers the path in the shared,
555
+ // ownerless linked-cases registry, so it carries the same bar as POST /api/cases/link.
556
+ if (customPath !== undefined) {
557
+ const denied = adminOnly(req, reply);
558
+ if (denied)
559
+ return denied;
560
+ }
549
561
  const casesDir = resolveCasesDir(user);
550
- const casePath = validatePathWithinBase(name, casesDir);
562
+ let casePath;
563
+ let intoEmptyDir = false;
564
+ if (customPath !== undefined) {
565
+ const prepared = await prepareNewCasePath(customPath, {
566
+ home: homedir(),
567
+ dataDir: getDataDir(),
568
+ casesDirs: [...new Set([casesDir, resolveCasesDir()])],
569
+ casesDirAdvice: 'clone there without a custom folder',
570
+ });
571
+ if (!prepared.ok) {
572
+ reply.code(prepared.code === 'UNREACHABLE'
573
+ ? 422
574
+ : prepared.code === 'NOT_FOUND'
575
+ ? 404
576
+ : prepared.code === 'EXISTS'
577
+ ? 409
578
+ : 400);
579
+ return createErrorResponse(prepared.code === 'UNREACHABLE'
580
+ ? ApiErrorCode.OPERATION_FAILED
581
+ : prepared.code === 'NOT_FOUND'
582
+ ? ApiErrorCode.NOT_FOUND
583
+ : prepared.code === 'EXISTS'
584
+ ? ApiErrorCode.ALREADY_EXISTS
585
+ : ApiErrorCode.INVALID_INPUT, prepared.code === 'EXISTS'
586
+ ? `${customPath} already has files in it. Pick an empty folder, or a folder that does not exist yet.`
587
+ : prepared.reason);
588
+ }
589
+ intoEmptyDir = prepared.existedEmpty;
590
+ casePath = prepared.path;
591
+ // The same folder listed as a local case in the cases dir would make deleting it remove files.
592
+ if (existsSync(join(casesDir, name))) {
593
+ reply.code(409);
594
+ return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
595
+ }
596
+ }
597
+ else {
598
+ casePath = validatePathWithinBase(name, casesDir);
599
+ }
551
600
  if (!casePath)
552
601
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
553
602
  // Reject a duplicate name across EVERY case kind before invoking git, so a
@@ -556,22 +605,52 @@ export function registerCaseRoutes(app, ctx) {
556
605
  const linkedCases = await readLinkedCases();
557
606
  const dockerCases = await readDockerCases(CODEMAN_CONFIG_DIR);
558
607
  const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
559
- if (existsSync(casePath) ||
608
+ const linkedAs = Object.entries(linkedCases).find(([, p]) => p === casePath)?.[0];
609
+ if (linkedAs && customPath !== undefined) {
610
+ reply.code(409);
611
+ return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `That folder is already the case "${linkedAs}"`);
612
+ }
613
+ if (
614
+ // A custom folder was judged by prepareNewCasePath (an existing empty one is fine).
615
+ (customPath === undefined && existsSync(casePath)) ||
560
616
  linkedCases[name] ||
561
617
  dockerCases.some((item) => item.name === name) ||
562
618
  remoteCases.some((item) => item.name === name)) {
563
619
  return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'Case already exists');
564
620
  }
565
- // git creates the leaf, not necessarily the case space above it.
621
+ // git creates the leaf, not necessarily the case space above it (a custom folder's parent
622
+ // already exists: prepareNewCasePath refused a missing one).
623
+ if (customPath === undefined) {
624
+ try {
625
+ mkdirSync(casesDir, { recursive: true });
626
+ }
627
+ catch (err) {
628
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
629
+ }
630
+ }
631
+ // git runs the new repository's hooks and filters on THIS machine once the clone finishes, and a container
632
+ // with the workspace bind-mounted can write both while it fetches. Like git status, fetch and worktrees,
633
+ // never clone at or inside a Docker case workspace. Fails closed when the Docker case list cannot be read.
566
634
  try {
567
- mkdirSync(casesDir, { recursive: true });
635
+ // Probed first (#516): a case's workspace can sit on a mount that stopped answering, and an unbounded
636
+ // realpath there would hold this request and a threadpool worker. Such a root is judged as spelled.
637
+ const roots = await Promise.all((await defaultDockerWorkspaces()).map(async (root) => (await probePathKind(root, { pastCap: true })) === 'unknown'
638
+ ? resolve(root)
639
+ : fs.realpath(root).catch(() => resolve(root))));
640
+ // A custom folder is already symlink-resolved; the default one is judged through its real cases dir.
641
+ const dest = customPath !== undefined ? casePath : join(await fs.realpath(casesDir).catch(() => casesDir), name);
642
+ if (roots.some((root) => isPathWithin(dest, root))) {
643
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_CLONE_REFUSAL);
644
+ }
568
645
  }
569
- catch (err) {
570
- return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
646
+ catch {
647
+ reply.code(503);
648
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Could not read the Docker case list, so Codeman cannot tell whether that folder is a Docker case workspace. Try again.');
571
649
  }
572
650
  const clone = await cloneRepository({
573
651
  repository: parsed.repository,
574
652
  destination: casePath,
653
+ ...(intoEmptyDir ? { intoEmptyDir: true } : {}),
575
654
  withoutCredentialHelpers: cloneWithoutCredentialHelpers(req),
576
655
  ...(ref ? { ref } : {}),
577
656
  ...(shallow ? { shallow: true } : {}),
@@ -628,6 +707,23 @@ export function registerCaseRoutes(app, ctx) {
628
707
  // problem, rather than deleting a tree the user just waited for.
629
708
  warnings.push(`Case scaffolding was incomplete: ${getErrorMessage(err)}`);
630
709
  }
710
+ if (customPath !== undefined) {
711
+ // Re-read right before writing so a case linked while git ran is not dropped. Like
712
+ // POST /api/cases/link the write is not serialized, so this only narrows the window.
713
+ try {
714
+ const fresh = await readLinkedCases();
715
+ if (fresh[name])
716
+ throw Object.assign(new Error(`Case "${name}" was linked while the clone ran`), { conflict: true });
717
+ fresh[name] = casePath;
718
+ await fs.writeFile(LINKED_CASES_FILE, JSON.stringify(fresh, null, 2));
719
+ }
720
+ catch (err) {
721
+ // The clone itself succeeded: keep the folder (the user waited for it) and say where it is.
722
+ const conflict = err.conflict;
723
+ reply.code(conflict ? 409 : 500);
724
+ return createErrorResponse(conflict ? ApiErrorCode.ALREADY_EXISTS : ApiErrorCode.OPERATION_FAILED, `Cloned into ${casePath} but could not register the case (${getErrorMessage(err)}). Use Link Existing to add it.`);
725
+ }
726
+ }
631
727
  ctx.broadcast(SseEvent.CaseCreated, { name, path: casePath });
632
728
  return {
633
729
  success: true,