aicodeman 1.41.0 → 1.42.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 (339) hide show
  1. package/README.md +101 -21
  2. package/dist/attachment-registry.d.ts +2 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +14 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +2 -0
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +13 -9
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/cli.d.ts +25 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +97 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config/buffer-limits.d.ts +9 -0
  15. package/dist/config/buffer-limits.d.ts.map +1 -1
  16. package/dist/config/buffer-limits.js +9 -0
  17. package/dist/config/buffer-limits.js.map +1 -1
  18. package/dist/config/cli-registry/schema.d.ts +10 -0
  19. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  20. package/dist/config/cli-registry/schema.js +26 -0
  21. package/dist/config/cli-registry/schema.js.map +1 -1
  22. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  23. package/dist/config/cli-registry/stock.js +89 -6
  24. package/dist/config/cli-registry/stock.js.map +1 -1
  25. package/dist/config/cli-registry/types.d.ts +61 -0
  26. package/dist/config/cli-registry/types.d.ts.map +1 -1
  27. package/dist/folder-listing.d.ts +94 -0
  28. package/dist/folder-listing.d.ts.map +1 -0
  29. package/dist/folder-listing.js +241 -0
  30. package/dist/folder-listing.js.map +1 -0
  31. package/dist/git-worktrees.d.ts +292 -0
  32. package/dist/git-worktrees.d.ts.map +1 -0
  33. package/dist/git-worktrees.js +791 -0
  34. package/dist/git-worktrees.js.map +1 -0
  35. package/dist/hooks-config.d.ts +36 -16
  36. package/dist/hooks-config.d.ts.map +1 -1
  37. package/dist/hooks-config.js +45 -20
  38. package/dist/hooks-config.js.map +1 -1
  39. package/dist/html-view-capabilities.d.ts +54 -0
  40. package/dist/html-view-capabilities.d.ts.map +1 -0
  41. package/dist/html-view-capabilities.js +103 -0
  42. package/dist/html-view-capabilities.js.map +1 -0
  43. package/dist/mcp-server.d.ts +114 -0
  44. package/dist/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp-server.js +918 -0
  46. package/dist/mcp-server.js.map +1 -0
  47. package/dist/mcp-sync.d.ts +5 -0
  48. package/dist/mcp-sync.d.ts.map +1 -1
  49. package/dist/mcp-sync.js +51 -5
  50. package/dist/mcp-sync.js.map +1 -1
  51. package/dist/mux-interface.d.ts +36 -1
  52. package/dist/mux-interface.d.ts.map +1 -1
  53. package/dist/remote-files.d.ts +37 -0
  54. package/dist/remote-files.d.ts.map +1 -1
  55. package/dist/remote-files.js +103 -0
  56. package/dist/remote-files.js.map +1 -1
  57. package/dist/remote-pane-history.d.ts +103 -0
  58. package/dist/remote-pane-history.d.ts.map +1 -0
  59. package/dist/remote-pane-history.js +258 -0
  60. package/dist/remote-pane-history.js.map +1 -0
  61. package/dist/remote-ssh-limiter.d.ts +16 -3
  62. package/dist/remote-ssh-limiter.d.ts.map +1 -1
  63. package/dist/remote-ssh-limiter.js +37 -6
  64. package/dist/remote-ssh-limiter.js.map +1 -1
  65. package/dist/services/unified-session-service.d.ts +8 -0
  66. package/dist/services/unified-session-service.d.ts.map +1 -1
  67. package/dist/services/unified-session-service.js +21 -0
  68. package/dist/services/unified-session-service.js.map +1 -1
  69. package/dist/session-activity.d.ts +51 -0
  70. package/dist/session-activity.d.ts.map +1 -1
  71. package/dist/session-activity.js +62 -0
  72. package/dist/session-activity.js.map +1 -1
  73. package/dist/session-submit-verifier.js +1 -1
  74. package/dist/session.d.ts +90 -2
  75. package/dist/session.d.ts.map +1 -1
  76. package/dist/session.js +247 -9
  77. package/dist/session.js.map +1 -1
  78. package/dist/tab-layout-legacy-order.d.ts.map +1 -1
  79. package/dist/tab-layout-legacy-order.js +72 -13
  80. package/dist/tab-layout-legacy-order.js.map +1 -1
  81. package/dist/tab-layout-service.d.ts +33 -2
  82. package/dist/tab-layout-service.d.ts.map +1 -1
  83. package/dist/tab-layout-service.js +102 -7
  84. package/dist/tab-layout-service.js.map +1 -1
  85. package/dist/tab-layout.d.ts +9 -0
  86. package/dist/tab-layout.d.ts.map +1 -1
  87. package/dist/tab-layout.js +15 -2
  88. package/dist/tab-layout.js.map +1 -1
  89. package/dist/tmux-manager.d.ts +130 -7
  90. package/dist/tmux-manager.d.ts.map +1 -1
  91. package/dist/tmux-manager.js +263 -36
  92. package/dist/tmux-manager.js.map +1 -1
  93. package/dist/types/session.d.ts +53 -0
  94. package/dist/types/session.d.ts.map +1 -1
  95. package/dist/types/session.js.map +1 -1
  96. package/dist/usage-telemetry.d.ts +44 -7
  97. package/dist/usage-telemetry.d.ts.map +1 -1
  98. package/dist/usage-telemetry.js +72 -7
  99. package/dist/usage-telemetry.js.map +1 -1
  100. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
  101. package/dist/utils/deepseek-cli-resolver.js +1 -1
  102. package/dist/utils/deepseek-cli-resolver.js.map +1 -1
  103. package/dist/web/approval-inbox.d.ts +24 -2
  104. package/dist/web/approval-inbox.d.ts.map +1 -1
  105. package/dist/web/approval-inbox.js +8 -2
  106. package/dist/web/approval-inbox.js.map +1 -1
  107. package/dist/web/middleware/auth.d.ts.map +1 -1
  108. package/dist/web/middleware/auth.js +33 -0
  109. package/dist/web/middleware/auth.js.map +1 -1
  110. package/dist/web/ports/session-port.d.ts +8 -1
  111. package/dist/web/ports/session-port.d.ts.map +1 -1
  112. package/dist/web/public/admin-ui.js.gz +0 -0
  113. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  114. package/dist/web/public/app.f04bb2a5.js +43 -0
  115. package/dist/web/public/app.f04bb2a5.js.br +0 -0
  116. package/dist/web/public/app.f04bb2a5.js.gz +0 -0
  117. package/dist/web/public/approvals-ui.js +4 -0
  118. package/dist/web/public/approvals-ui.js.br +0 -0
  119. package/dist/web/public/approvals-ui.js.gz +0 -0
  120. package/dist/web/public/{constants.bc9c1f63.js → constants.12b0c247.js} +395 -7
  121. package/dist/web/public/constants.12b0c247.js.br +0 -0
  122. package/dist/web/public/constants.12b0c247.js.gz +0 -0
  123. package/dist/web/public/cron-ui.js.gz +0 -0
  124. package/dist/web/public/entrance-animations.js.gz +0 -0
  125. package/dist/web/public/gesture/gesture-codeman.js +112 -6
  126. package/dist/web/public/git-status-ui.js.gz +0 -0
  127. package/dist/web/public/home-sessions.js.gz +0 -0
  128. package/dist/web/public/host-wake-ui.js.gz +0 -0
  129. package/dist/web/public/i18n.06659676.js +2 -0
  130. package/dist/web/public/i18n.06659676.js.br +0 -0
  131. package/dist/web/public/i18n.06659676.js.gz +0 -0
  132. package/dist/web/public/image-input.fcdcb46c.js +462 -0
  133. package/dist/web/public/image-input.fcdcb46c.js.br +0 -0
  134. package/dist/web/public/image-input.fcdcb46c.js.gz +0 -0
  135. package/dist/web/public/index.html +169 -24
  136. package/dist/web/public/index.html.br +0 -0
  137. package/dist/web/public/index.html.gz +0 -0
  138. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  139. package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.b42f539b.js} +124 -44
  140. package/dist/web/public/keyboard-accessory.b42f539b.js.br +0 -0
  141. package/dist/web/public/keyboard-accessory.b42f539b.js.gz +0 -0
  142. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  143. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  144. package/dist/web/public/mobile-overview.js +11 -2
  145. package/dist/web/public/mobile-overview.js.br +0 -0
  146. package/dist/web/public/mobile-overview.js.gz +0 -0
  147. package/dist/web/public/mobile.2dff1580.css +1 -0
  148. package/dist/web/public/mobile.2dff1580.css.br +0 -0
  149. package/dist/web/public/mobile.2dff1580.css.gz +0 -0
  150. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  151. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  152. package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
  153. package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
  154. package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
  155. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  156. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  157. package/dist/web/public/readmymind-ui.js.gz +0 -0
  158. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  159. package/dist/web/public/{respawn-ui.ff0dae4c.js → respawn-ui.d7adcf6a.js} +1 -1
  160. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  161. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  162. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  163. package/dist/web/public/session-lineage.js.gz +0 -0
  164. package/dist/web/public/session-ui.d52ff6aa.js +100 -0
  165. package/dist/web/public/session-ui.d52ff6aa.js.br +0 -0
  166. package/dist/web/public/session-ui.d52ff6aa.js.gz +0 -0
  167. package/dist/web/public/settings-ui.3a22bd2a.js +97 -0
  168. package/dist/web/public/settings-ui.3a22bd2a.js.br +0 -0
  169. package/dist/web/public/settings-ui.3a22bd2a.js.gz +0 -0
  170. package/dist/web/public/spreadsheet-preview-worker.js +11 -0
  171. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  172. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  173. package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
  174. package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
  175. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  176. package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
  177. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  178. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  179. package/dist/web/public/styles.ba6a9b84.css +1 -0
  180. package/dist/web/public/styles.ba6a9b84.css.br +0 -0
  181. package/dist/web/public/styles.ba6a9b84.css.gz +0 -0
  182. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  183. package/dist/web/public/sw.js +6 -2
  184. package/dist/web/public/sw.js.br +0 -0
  185. package/dist/web/public/sw.js.gz +0 -0
  186. package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
  187. package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
  188. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  189. package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
  190. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  191. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  192. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  193. package/dist/web/public/terminal-split.js.gz +0 -0
  194. package/dist/web/public/terminal-tile.js +45 -24
  195. package/dist/web/public/terminal-tile.js.br +0 -0
  196. package/dist/web/public/terminal-tile.js.gz +0 -0
  197. package/dist/web/public/terminal-ui.0a7ccc29.js +2 -0
  198. package/dist/web/public/terminal-ui.0a7ccc29.js.br +0 -0
  199. package/dist/web/public/terminal-ui.0a7ccc29.js.gz +0 -0
  200. package/dist/web/public/tile-grid.js +439 -18
  201. package/dist/web/public/tile-grid.js.br +0 -0
  202. package/dist/web/public/tile-grid.js.gz +0 -0
  203. package/dist/web/public/ultracode-panel.js.gz +0 -0
  204. package/dist/web/public/ultracode-windows.js.gz +0 -0
  205. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  206. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  207. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  208. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  209. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  210. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  211. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  213. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  214. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  215. package/dist/web/public/vendor/xterm.css.gz +0 -0
  216. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  217. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  218. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  219. package/dist/web/public/webview-tabs.js +12 -0
  220. package/dist/web/public/webview-tabs.js.br +0 -0
  221. package/dist/web/public/webview-tabs.js.gz +0 -0
  222. package/dist/web/public/whats-new.json +13 -0
  223. package/dist/web/route-helpers.d.ts +8 -0
  224. package/dist/web/route-helpers.d.ts.map +1 -1
  225. package/dist/web/route-helpers.js +8 -0
  226. package/dist/web/route-helpers.js.map +1 -1
  227. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  228. package/dist/web/routes/admin-routes.js +4 -1
  229. package/dist/web/routes/admin-routes.js.map +1 -1
  230. package/dist/web/routes/approval-routes.d.ts.map +1 -1
  231. package/dist/web/routes/approval-routes.js +12 -10
  232. package/dist/web/routes/approval-routes.js.map +1 -1
  233. package/dist/web/routes/case-routes.d.ts.map +1 -1
  234. package/dist/web/routes/case-routes.js +10 -3
  235. package/dist/web/routes/case-routes.js.map +1 -1
  236. package/dist/web/routes/file-routes.d.ts +46 -1
  237. package/dist/web/routes/file-routes.d.ts.map +1 -1
  238. package/dist/web/routes/file-routes.js +214 -27
  239. package/dist/web/routes/file-routes.js.map +1 -1
  240. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  241. package/dist/web/routes/hook-event-routes.js +8 -1
  242. package/dist/web/routes/hook-event-routes.js.map +1 -1
  243. package/dist/web/routes/html-view-routes.d.ts +60 -0
  244. package/dist/web/routes/html-view-routes.d.ts.map +1 -0
  245. package/dist/web/routes/html-view-routes.js +288 -0
  246. package/dist/web/routes/html-view-routes.js.map +1 -0
  247. package/dist/web/routes/index.d.ts +2 -0
  248. package/dist/web/routes/index.d.ts.map +1 -1
  249. package/dist/web/routes/index.js +2 -0
  250. package/dist/web/routes/index.js.map +1 -1
  251. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  252. package/dist/web/routes/ralph-routes.js +19 -14
  253. package/dist/web/routes/ralph-routes.js.map +1 -1
  254. package/dist/web/routes/reboot-restore-routes.js +2 -2
  255. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  256. package/dist/web/routes/session-routes.d.ts +8 -5
  257. package/dist/web/routes/session-routes.d.ts.map +1 -1
  258. package/dist/web/routes/session-routes.js +462 -109
  259. package/dist/web/routes/session-routes.js.map +1 -1
  260. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  261. package/dist/web/routes/status-telemetry-routes.js +24 -6
  262. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  263. package/dist/web/routes/system-routes.d.ts +9 -0
  264. package/dist/web/routes/system-routes.d.ts.map +1 -1
  265. package/dist/web/routes/system-routes.js +91 -12
  266. package/dist/web/routes/system-routes.js.map +1 -1
  267. package/dist/web/routes/worktree-routes.d.ts +52 -0
  268. package/dist/web/routes/worktree-routes.d.ts.map +1 -0
  269. package/dist/web/routes/worktree-routes.js +220 -0
  270. package/dist/web/routes/worktree-routes.js.map +1 -0
  271. package/dist/web/schemas.d.ts +45 -0
  272. package/dist/web/schemas.d.ts.map +1 -1
  273. package/dist/web/schemas.js +105 -0
  274. package/dist/web/schemas.js.map +1 -1
  275. package/dist/web/screen-blocked.d.ts +80 -0
  276. package/dist/web/screen-blocked.d.ts.map +1 -0
  277. package/dist/web/screen-blocked.js +106 -0
  278. package/dist/web/screen-blocked.js.map +1 -0
  279. package/dist/web/server.d.ts +6 -1
  280. package/dist/web/server.d.ts.map +1 -1
  281. package/dist/web/server.js +73 -14
  282. package/dist/web/server.js.map +1 -1
  283. package/dist/web/session-listener-wiring.d.ts +1 -0
  284. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  285. package/dist/web/session-listener-wiring.js +20 -0
  286. package/dist/web/session-listener-wiring.js.map +1 -1
  287. package/dist/web/session-wait-registry.d.ts +27 -9
  288. package/dist/web/session-wait-registry.d.ts.map +1 -1
  289. package/dist/web/session-wait-registry.js +36 -13
  290. package/dist/web/session-wait-registry.js.map +1 -1
  291. package/dist/web/sse-events.d.ts +12 -4
  292. package/dist/web/sse-events.d.ts.map +1 -1
  293. package/dist/web/sse-events.js +12 -4
  294. package/dist/web/sse-events.js.map +1 -1
  295. package/package.json +3 -2
  296. package/skills/codeman/SKILL.md +47 -27
  297. package/skills/codeman/preamble.sh +14 -11
  298. package/skills/codeman/reference/endpoints.md +1 -1
  299. package/skills/codeman/reference/recipes.md +12 -9
  300. package/skills/codeman/reference/verbs.md +3 -1
  301. package/dist/web/public/app.6f8552e1.js +0 -43
  302. package/dist/web/public/app.6f8552e1.js.br +0 -0
  303. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  304. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  305. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  306. package/dist/web/public/i18n.a9840ea2.js +0 -2
  307. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  308. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  309. package/dist/web/public/image-input.e00471c6.js +0 -283
  310. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  311. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  312. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
  313. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  314. package/dist/web/public/mobile.111b187d.css +0 -1
  315. package/dist/web/public/mobile.111b187d.css.br +0 -0
  316. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  317. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  318. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  319. package/dist/web/public/respawn-ui.ff0dae4c.js.br +0 -0
  320. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  321. package/dist/web/public/session-ui.f731602c.js +0 -100
  322. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  323. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  324. package/dist/web/public/settings-ui.4e33390f.js +0 -97
  325. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  326. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  327. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  328. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  329. package/dist/web/public/styles.81a72906.css +0 -1
  330. package/dist/web/public/styles.81a72906.css.br +0 -0
  331. package/dist/web/public/styles.81a72906.css.gz +0 -0
  332. package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
  333. package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
  334. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  335. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  336. package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
  337. package/dist/web/public/terminal-ui.b7267005.js +0 -2
  338. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  339. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
@@ -12,8 +12,10 @@ import { createHash, randomBytes } from 'node:crypto';
12
12
  import { homedir } from 'node:os';
13
13
  import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
14
14
  import { compileFileQuery } from '../../utils/file-query.js';
15
+ import { describeUnknownPath, probePathKind } from '../../utils/bounded-path-probe.js';
15
16
  import { fileStreamManager } from '../../file-stream-manager.js';
16
- import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, isSupportedAttachmentExtension, registerExternalAttachment, TEXT_ATTACHMENT_EXTENSIONS, VIDEO_ATTACHMENT_EXTENSIONS, } from '../../attachment-registry.js';
17
+ import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, getAttachmentType, isSupportedAttachmentExtension, registerExternalAttachment, TEXT_ATTACHMENT_EXTENSIONS, VIDEO_ATTACHMENT_EXTENSIONS, } from '../../attachment-registry.js';
18
+ import { FolderListingError, folderFilePath, folderGrants, folderAllowedForUser, listFolder, validFolderFileName, } from '../../folder-listing.js';
17
19
  import { generateFirstPageThumbnail } from '../../document-thumbnailer.js';
18
20
  import { getOfficePreviewPdfPath, getPreviewPdfDownloadName } from '../../document-preview-cache.js';
19
21
  import { sanitizeAttachmentHistoryItem } from '../../session-attachment-history.js';
@@ -125,8 +127,8 @@ function sendRawStream(reply, content, cleanup) {
125
127
  });
126
128
  content.pipe(reply.raw);
127
129
  }
128
- /** Byte source for a LOCAL file (the historical, only path). */
129
- function localFileSource(resolvedPath) {
130
+ /** Byte source for a LOCAL file (the historical, only path). Shared with html-view-routes.ts. */
131
+ export function localFileSource(resolvedPath) {
130
132
  return (range) => ({
131
133
  content: createReadStream(resolvedPath, range ? { start: range.start, end: range.end } : undefined),
132
134
  });
@@ -145,8 +147,11 @@ function localFileSource(resolvedPath) {
145
147
  * `open` supplies the bytes for the (optional) range, which is what lets a remote
146
148
  * case reuse this instead of re-implementing the 206/416 contract: same headers,
147
149
  * same status codes, whether the file sits on this host or behind an ssh pipe.
150
+ *
151
+ * Exported for the rendered-HTML route (html-view-routes.ts), so a page's own
152
+ * `<video>`/`<audio>` can seek as well.
148
153
  */
149
- function sendFileBody(reply, size, rangeHeader, open) {
154
+ export function sendFileBody(reply, size, rangeHeader, open) {
150
155
  reply.header('Accept-Ranges', 'bytes');
151
156
  const range = parseByteRange(rangeHeader, size);
152
157
  if (range.kind === 'unsatisfiable') {
@@ -178,7 +183,9 @@ async function serveRawFile(reply, target, size, fileName, extension, download,
178
183
  // would be stored XSS. SVG was always here; HTML/HTM join it now that the text
179
184
  // family is servable, so widening what can be READ never widened what can RUN.
180
185
  // The preview overlay reads these through `fetch()`, which ignores the
181
- // disposition, so a clicked .html still shows its source.
186
+ // disposition, so the source view of a clicked .html still works. Rendering it
187
+ // as a page is html-view-routes.ts's job (sandboxed, opaque origin), never this
188
+ // route's.
182
189
  const markupOnly = extension === 'svg' || extension === 'html' || extension === 'htm';
183
190
  if (download || markupOnly) {
184
191
  reply.header('Content-Type', markupOnly ? 'application/octet-stream' : MIME_TYPES[extension] || 'application/octet-stream');
@@ -296,6 +303,32 @@ async function resolveServableRemoteAttachment(reply, record, scope) {
296
303
  }
297
304
  return { path: probe.realPath, probe };
298
305
  }
306
+ /**
307
+ * Stream a guard-checked file ({@link resolveServableAttachmentPath}) — the body of
308
+ * the attachment raw route, shared with the folder-grid file route.
309
+ *
310
+ * A remote file streams over ssh exactly like file-raw, with the same 200/206/416
311
+ * contract, and its size comes from the guard's own re-probe — so serving a remote
312
+ * file needs no stat the local branch would not also need.
313
+ */
314
+ async function serveServableRaw(reply, servable, remote, fileName, extension, query) {
315
+ try {
316
+ const target = servable.probe && remote
317
+ ? { kind: 'remote', resolvedPath: servable.path, relativePath: '', remote, probe: servable.probe }
318
+ : { kind: 'local', resolvedPath: servable.path, relativePath: '' };
319
+ const size = servable.probe ? servable.probe.size : (await fs.stat(servable.path)).size;
320
+ if (exceedsXlsxPreviewLimit(extension, query, size)) {
321
+ sendXlsxPreviewTooLarge(reply, size);
322
+ return;
323
+ }
324
+ await serveRawFile(reply, target, size, fileName, extension, query.download === 'true', query.range);
325
+ }
326
+ catch (err) {
327
+ reply
328
+ .code(err instanceof RemoteFileAccessError ? 502 : 500)
329
+ .send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to read file: ${getErrorMessage(err)}`));
330
+ }
331
+ }
299
332
  /**
300
333
  * Convert a DOCX/PPTX to a single-PDF preview (LibreOffice when available) and
301
334
  * stream it inline. PDF/PNG and text formats don't need conversion — callers
@@ -350,6 +383,14 @@ function getKnownSessionWorkingDir(ctx, sessionId, reply, req) {
350
383
  // wrapper exists for the callers that only need the workspace PATH.
351
384
  return getKnownSessionFileScope(ctx, sessionId, reply, req)?.workingDir;
352
385
  }
386
+ /**
387
+ * A Docker case is a LOCAL session for the file routes (the workspace is a
388
+ * bind mount, so its paths are host paths), but `~` inside the container is the
389
+ * container's own seeded home, not this host's. Expanding `~/x.png` against the
390
+ * host's home would open a different file than the one the agent printed, so
391
+ * the form is refused instead of guessed.
392
+ */
393
+ export const DOCKER_HOME_PATH_ERROR = 'A ~/ path inside a Docker case is the container home, which this server cannot read; use the absolute path';
353
394
  /**
354
395
  * The file-access scope of a session, live or persisted — the `workingDir`-only
355
396
  * variant of {@link getKnownSessionWorkingDir}, plus the remote metadata the routes
@@ -362,11 +403,11 @@ function getKnownSessionFileScope(ctx, sessionId, reply, req) {
362
403
  const user = getAuthUser(req);
363
404
  const liveSession = ctx.sessions.get(sessionId);
364
405
  if (liveSession && canAccessOwned(user, liveSession.owner)) {
365
- return { workingDir: liveSession.workingDir, remote: liveSession.remote };
406
+ return { workingDir: liveSession.workingDir, remote: liveSession.remote, docker: !!liveSession.docker };
366
407
  }
367
408
  const stored = ctx.store.getSession(sessionId);
368
409
  if (stored && canAccessOwned(user, stored.owner)) {
369
- return { workingDir: stored.workingDir, remote: stored.remote };
410
+ return { workingDir: stored.workingDir, remote: stored.remote, docker: !!stored.docker };
370
411
  }
371
412
  reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`));
372
413
  return undefined;
@@ -1725,6 +1766,10 @@ export function registerFileRoutes(app, ctx) {
1725
1766
  reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing attachment path'));
1726
1767
  return;
1727
1768
  }
1769
+ if (session.docker && body.path.startsWith('~/')) {
1770
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_HOME_PATH_ERROR));
1771
+ return;
1772
+ }
1728
1773
  try {
1729
1774
  // A remote case registers a path that lives on the REMOTE host: the guard and
1730
1775
  // the reachability check happen there (#415 — this is the path a clicked
@@ -1839,26 +1884,11 @@ export function registerFileRoutes(app, ctx) {
1839
1884
  });
1840
1885
  if (!servable)
1841
1886
  return;
1842
- try {
1843
- // A remote record streams over ssh exactly like file-raw, with the same
1844
- // 200/206/416 contract, and its size comes from the guard's own re-probe — so
1845
- // serving a remote attachment needs no stat the local branch would not also need.
1846
- const remote = session.remote;
1847
- const target = servable.probe && remote
1848
- ? { kind: 'remote', resolvedPath: servable.path, relativePath: '', remote, probe: servable.probe }
1849
- : { kind: 'local', resolvedPath: servable.path, relativePath: '' };
1850
- const size = servable.probe ? servable.probe.size : (await fs.stat(servable.path)).size;
1851
- if (exceedsXlsxPreviewLimit(record.extension, { preview, download }, size)) {
1852
- sendXlsxPreviewTooLarge(reply, size);
1853
- return;
1854
- }
1855
- await serveRawFile(reply, target, size, record.fileName, record.extension, download === 'true', req.headers.range);
1856
- }
1857
- catch (err) {
1858
- reply
1859
- .code(err instanceof RemoteFileAccessError ? 502 : 500)
1860
- .send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to read file: ${getErrorMessage(err)}`));
1861
- }
1887
+ await serveServableRaw(reply, servable, session.remote, record.fileName, record.extension, {
1888
+ download,
1889
+ preview,
1890
+ range: req.headers.range,
1891
+ });
1862
1892
  });
1863
1893
  // Serve a converted PDF preview of a registered attachment by id. Office docs
1864
1894
  // convert server-side; PDF/PNG/text redirect to the raw route.
@@ -1910,6 +1940,163 @@ export function registerFileRoutes(app, ctx) {
1910
1940
  }
1911
1941
  await serveThumbnail(reply, servable.path, record.extension);
1912
1942
  });
1943
+ // ===== Folder grid =====
1944
+ // Grant + list a folder (absolute or `~/` on the session's host) for the preview
1945
+ // grid. Same guard as a single external attachment; the grid then fetches files by
1946
+ // `folderId + name`, so no browser request carries a path. See folder-listing.ts.
1947
+ app.post('/api/sessions/:id/folders', async (req, reply) => {
1948
+ const { id } = req.params;
1949
+ const scope = getKnownSessionFileScope(ctx, id, reply, req);
1950
+ if (!scope)
1951
+ return;
1952
+ const body = (req.body || {});
1953
+ if (!body.path || typeof body.path !== 'string') {
1954
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing folder path'));
1955
+ return;
1956
+ }
1957
+ // A bare `~` expands to this host's home too, not the container's.
1958
+ if (scope.docker && (body.path === '~' || body.path.startsWith('~/'))) {
1959
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_HOME_PATH_ERROR));
1960
+ return;
1961
+ }
1962
+ try {
1963
+ const listing = await listFolder(id, body.path, {
1964
+ sessionWorkingDir: scope.workingDir,
1965
+ remote: scope.remote,
1966
+ user: getAuthUser(req),
1967
+ });
1968
+ return { success: true, data: listing };
1969
+ }
1970
+ catch (err) {
1971
+ if (err instanceof FolderListingError) {
1972
+ const code = err.statusCode === 404
1973
+ ? ApiErrorCode.NOT_FOUND
1974
+ : err.statusCode >= 500
1975
+ ? ApiErrorCode.OPERATION_FAILED
1976
+ : ApiErrorCode.INVALID_INPUT;
1977
+ reply.code(err.statusCode).send(createErrorResponse(code, err.message));
1978
+ return;
1979
+ }
1980
+ return reply
1981
+ .code(500)
1982
+ .send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to list folder: ${getErrorMessage(err)}`));
1983
+ }
1984
+ });
1985
+ /**
1986
+ * One file of a granted folder, guard-checked like an attachment: the name is a
1987
+ * bare file name of a supported type, and the joined path goes through the same
1988
+ * {@link resolveServableAttachmentPath} (blocklist, workspace confinement, symlink
1989
+ * re-resolution, remote re-probe) an attachment by id does. The multi-user rule
1990
+ * ({@link folderAllowedForUser}) runs again on the resolved file: a symlink inside
1991
+ * the granted folder must not lead into another user's space.
1992
+ */
1993
+ async function resolveFolderFile(req, reply) {
1994
+ const { id, folderId, name } = req.params;
1995
+ const scope = getKnownSessionFileScope(ctx, id, reply, req);
1996
+ if (!scope)
1997
+ return null;
1998
+ const grant = folderGrants.get(id, folderId);
1999
+ if (!grant) {
2000
+ reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'Folder not found'));
2001
+ return null;
2002
+ }
2003
+ const fileName = validFolderFileName(name);
2004
+ if (!fileName) {
2005
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid file name'));
2006
+ return null;
2007
+ }
2008
+ const filePath = folderFilePath(grant.dir, fileName);
2009
+ if (!scope.remote) {
2010
+ // Bounded, like the folder itself: the grant may point at a mount that has stopped answering since.
2011
+ const kind = await probePathKind(filePath, { pastCap: true });
2012
+ if (kind === 'unknown') {
2013
+ reply
2014
+ .code(503)
2015
+ .send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, describeUnknownPath('File', filePath, { pastCap: true })));
2016
+ return null;
2017
+ }
2018
+ if (kind !== 'file') {
2019
+ reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found'));
2020
+ return null;
2021
+ }
2022
+ }
2023
+ const extension = extname(fileName).toLowerCase().replace(/^\./, '');
2024
+ const record = {
2025
+ attachmentId: '',
2026
+ sessionId: id,
2027
+ filePath,
2028
+ fileName,
2029
+ extension,
2030
+ attachmentType: getAttachmentType(extension),
2031
+ size: 0,
2032
+ mtimeMs: 0,
2033
+ timestamp: 0,
2034
+ source: 'external',
2035
+ };
2036
+ const servable = await resolveServableAttachmentPath(reply, record, scope);
2037
+ if (!servable)
2038
+ return null;
2039
+ // The requested NAME only opens the door; registration judges the file a
2040
+ // symlink resolves to (supported extension, regular file) and so does this.
2041
+ // Without it `shot.png -> ../secret.bin` is served, and a FIFO or a device
2042
+ // behind a supported name parks a worker on an open that never returns.
2043
+ if (!isSupportedAttachmentExtension(extname(servable.path))) {
2044
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Unsupported attachment type'));
2045
+ return null;
2046
+ }
2047
+ const isRegularFile = scope.remote
2048
+ ? servable.probe?.kind === 'file'
2049
+ : await fs.stat(servable.path).then((stat) => stat.isFile(), () => false);
2050
+ if (!isRegularFile) {
2051
+ reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Attachment path is not a file'));
2052
+ return null;
2053
+ }
2054
+ if (!folderAllowedForUser(servable.path, getAuthUser(req), scope)) {
2055
+ reply.code(403).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'File is outside your space'));
2056
+ return null;
2057
+ }
2058
+ return { scope, servable, name: fileName, extension };
2059
+ }
2060
+ app.get('/api/sessions/:id/folders/:folderId/files/:name/raw', async (req, reply) => {
2061
+ const file = await resolveFolderFile(req, reply);
2062
+ if (!file)
2063
+ return;
2064
+ const { download, preview } = req.query;
2065
+ await serveServableRaw(reply, file.servable, file.scope.remote, file.name, file.extension, {
2066
+ download,
2067
+ preview,
2068
+ range: req.headers.range,
2069
+ });
2070
+ });
2071
+ app.get('/api/sessions/:id/folders/:folderId/files/:name/preview', async (req, reply) => {
2072
+ const file = await resolveFolderFile(req, reply);
2073
+ if (!file)
2074
+ return;
2075
+ if (file.extension !== 'docx' && file.extension !== 'pptx') {
2076
+ const { id, folderId } = req.params;
2077
+ reply.redirect(`/api/sessions/${encodeURIComponent(id)}/folders/${encodeURIComponent(folderId)}/files/${encodeURIComponent(file.name)}/raw`);
2078
+ return;
2079
+ }
2080
+ if (file.servable.probe) {
2081
+ reply
2082
+ .code(400)
2083
+ .send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Office document preview is not available for files in a remote (SSH) case'));
2084
+ return;
2085
+ }
2086
+ await serveConvertedPreview(reply, file.servable.path, file.name, file.extension);
2087
+ });
2088
+ app.get('/api/sessions/:id/folders/:folderId/files/:name/thumbnail', async (req, reply) => {
2089
+ const file = await resolveFolderFile(req, reply);
2090
+ if (!file)
2091
+ return;
2092
+ if (file.servable.probe) {
2093
+ reply
2094
+ .code(400)
2095
+ .send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Thumbnails are not available for files in a remote (SSH) case'));
2096
+ return;
2097
+ }
2098
+ await serveThumbnail(reply, file.servable.path, file.extension);
2099
+ });
1913
2100
  // Serve converted document previews for a workspace-relative path. DOCX/PPTX
1914
2101
  // are converted to PDF via LibreOffice; PDF/PNG/text preview through file-raw.
1915
2102
  app.get('/api/sessions/:id/file-preview', async (req, reply) => {