wind-agent-cli 1.37.2 → 1.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/LICENSE +718 -718
  2. package/README.md +1 -1
  3. package/dist/api-docs.html +901 -901
  4. package/dist/assets/{CodeEditor-B2Wf7Fg0.js → CodeEditor-BqmxpZ_D.js} +1 -1
  5. package/dist/assets/{FileTree-N2IkkHlv.js → FileTree-DLE7Ioai.js} +1 -1
  6. package/dist/assets/{GitPanel-DZCeuFZ2.js → GitPanel-f8RMRa8D.js} +1 -1
  7. package/dist/assets/{MarkdownPreview-C0tqYnVu.js → MarkdownPreview-93h_1tJa.js} +1 -1
  8. package/dist/assets/{PluginTabContent-Bnuf-fb8.js → PluginTabContent-DU1jjkXP.js} +1 -1
  9. package/dist/assets/{Shell-BfFnMZ-f.js → Shell-CN_2MRIM.js} +1 -1
  10. package/dist/assets/{StandaloneShell-QmyYwNAl.js → StandaloneShell-D0avS8UE.js} +1 -1
  11. package/dist/assets/{TaskMasterPanel-fMsmvN9L.js → TaskMasterPanel-Cpf1Q5Ph.js} +3 -3
  12. package/dist/assets/{abnfDiagram-N423BO3Z-DjPU7HLs.js → abnfDiagram-N423BO3Z-p3aJIm4T.js} +1 -1
  13. package/dist/assets/{arc-Pdq6Tvr3.js → arc-CTeud6Fl.js} +1 -1
  14. package/dist/assets/{architectureDiagram-T3A2C74G-D0JnT0Vh.js → architectureDiagram-T3A2C74G-CG7kRCJU.js} +1 -1
  15. package/dist/assets/{blockDiagram-VBNYF7ZC-DpaEf3mv.js → blockDiagram-VBNYF7ZC-oSjQ1SgE.js} +1 -1
  16. package/dist/assets/{c4Diagram-5PPSVZJV-MpqlPGFi.js → c4Diagram-5PPSVZJV-RkzF0DJV.js} +1 -1
  17. package/dist/assets/channel-BfQbarn9.js +1 -0
  18. package/dist/assets/{chunk-2GRJ4B5K-DoSdNo8u.js → chunk-2GRJ4B5K-B07uZ07O.js} +1 -1
  19. package/dist/assets/{chunk-2Q5K7J3B-CE7I3uMn.js → chunk-2Q5K7J3B-DNSfO7xS.js} +1 -1
  20. package/dist/assets/{chunk-5RXB4S5H-B4UQ8snD.js → chunk-5RXB4S5H-BTeYcgyb.js} +1 -1
  21. package/dist/assets/{chunk-5VM5RSS4-B55XgqHZ.js → chunk-5VM5RSS4-B7vZV0zC.js} +1 -1
  22. package/dist/assets/{chunk-6Q2QTUOP-BS1KpefW.js → chunk-6Q2QTUOP-DuS_xUGG.js} +1 -1
  23. package/dist/assets/{chunk-GF5L2VYU-DkEPedJP.js → chunk-GF5L2VYU-CKLZ3iVD.js} +1 -1
  24. package/dist/assets/{chunk-JWPE2WC7-DOtkNhMm.js → chunk-JWPE2WC7-DD2hcXvs.js} +1 -1
  25. package/dist/assets/{chunk-KBJHAD2P-BXLjviqM.js → chunk-KBJHAD2P-Cz-l38qp.js} +1 -1
  26. package/dist/assets/{chunk-RYQCIY6F-C50fjNNz.js → chunk-RYQCIY6F-DGFIU1YM.js} +1 -1
  27. package/dist/assets/{chunk-XXDRQBXY-QBYqCkb7.js → chunk-XXDRQBXY-CmfzoYFl.js} +1 -1
  28. package/dist/assets/classDiagram-JCYQIIEL-Bpfe3kq7.js +1 -0
  29. package/dist/assets/classDiagram-v2-OCEON4UE-Bpfe3kq7.js +1 -0
  30. package/dist/assets/{code-xml-VK1YfShP.js → code-xml-CLpHzEOP.js} +1 -1
  31. package/dist/assets/{cose-bilkent-JH36ORCC-DrVTo1Nh.js → cose-bilkent-JH36ORCC-DlFcLXCB.js} +1 -1
  32. package/dist/assets/{cynefin-VYW2F7L2-DnJa_OlQ.js → cynefin-VYW2F7L2-P1HecReO.js} +1 -1
  33. package/dist/assets/{cynefinDiagram-MW4NZA55-CCxBjw9E.js → cynefinDiagram-MW4NZA55-COgduz5P.js} +1 -1
  34. package/dist/assets/{dagre-VZM6K2ZE-C_GvE4lu.js → dagre-VZM6K2ZE-Bbb-siGp.js} +1 -1
  35. package/dist/assets/{diagram-7IWD3JNH-pFzs9HEu.js → diagram-7IWD3JNH-9CWGFMYR.js} +1 -1
  36. package/dist/assets/{diagram-B4RE2ZJO-CcehQC7x.js → diagram-B4RE2ZJO-TWK59T6V.js} +1 -1
  37. package/dist/assets/{diagram-LBJQPF4R-hfKcrrxe.js → diagram-LBJQPF4R-ErMC9yaa.js} +1 -1
  38. package/dist/assets/{diagram-Q27KOJAE-C1semJfK.js → diagram-Q27KOJAE-Dt_-C7hV.js} +1 -1
  39. package/dist/assets/{diagram-UB23O5K3-57hvXQXO.js → diagram-UB23O5K3-BJ1DnRAF.js} +1 -1
  40. package/dist/assets/{ebnfDiagram-BXEA7PRR-9KIAlAVw.js → ebnfDiagram-BXEA7PRR-CE1YaQ9s.js} +1 -1
  41. package/dist/assets/{erDiagram-JOGREHBK-B7OzJ767.js → erDiagram-JOGREHBK-Bqk7N9aY.js} +1 -1
  42. package/dist/assets/{flowDiagram-UKHOOZJN-CJV8fQPR.js → flowDiagram-UKHOOZJN-DEr2II5V.js} +1 -1
  43. package/dist/assets/{ganttDiagram-PKOTCBZU-CbMNWWQu.js → ganttDiagram-PKOTCBZU-CWYAUeYN.js} +1 -1
  44. package/dist/assets/{gitGraphDiagram-DS77QQ5N-I_vZ5waO.js → gitGraphDiagram-DS77QQ5N-tdEcEIM6.js} +1 -1
  45. package/dist/assets/{index-ZHwIojzi.js → index-B-sTfSZq.js} +1 -1
  46. package/dist/assets/{index-GFhXZyl2.js → index-BSe3UiFA.js} +1 -1
  47. package/dist/assets/{index-DFnVoqpx.js → index-B_FDMWar.js} +5 -5
  48. package/dist/assets/{index-DlSeAkw_.js → index-CWJtg2pR.js} +1 -1
  49. package/dist/assets/{infoDiagram-6WML65LV-Dh20Tsbq.js → infoDiagram-6WML65LV-A3IqRUnO.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DH2Sm3ko.js → ishikawaDiagram-WSZJBQD7-B2uL6bkK.js} +1 -1
  51. package/dist/assets/{journeyDiagram-NVQOT4AX-BNFQqlVu.js → journeyDiagram-NVQOT4AX-D9JB-hRW.js} +1 -1
  52. package/dist/assets/{kanban-definition-27J2QSJJ-B3iL0ztJ.js → kanban-definition-27J2QSJJ-DmlgiV7K.js} +1 -1
  53. package/dist/assets/{linear-Cwe9dEKA.js → linear-Ct_eoJQk.js} +1 -1
  54. package/dist/assets/{mermaid.core-BYRQSshi.js → mermaid.core-CQ698HNn.js} +5 -5
  55. package/dist/assets/{mindmap-definition-FAOFIHXS-BgfBYMzr.js → mindmap-definition-FAOFIHXS-DGtMgQhf.js} +1 -1
  56. package/dist/assets/{pegDiagram-VL7TDLO6-CNYpECr8.js → pegDiagram-VL7TDLO6-CO6vP4n9.js} +1 -1
  57. package/dist/assets/{pieDiagram-7S7Q4E2Y-DeSrxc4I.js → pieDiagram-7S7Q4E2Y-DMkt_Zfr.js} +1 -1
  58. package/dist/assets/{quadrantDiagram-CIZ2JOQS-BXVVQsa6.js → quadrantDiagram-CIZ2JOQS-DgNxRz6t.js} +1 -1
  59. package/dist/assets/{railroadDiagram-AXF67PYL-JTroIW8F.js → railroadDiagram-AXF67PYL-DKz9UqA9.js} +1 -1
  60. package/dist/assets/{requirementDiagram-LRYGKXZP-C1Z_inLz.js → requirementDiagram-LRYGKXZP-L84XyGgV.js} +1 -1
  61. package/dist/assets/{sankeyDiagram-W5VNT64P-DCJgKFUk.js → sankeyDiagram-W5VNT64P-CQqWbJ3j.js} +1 -1
  62. package/dist/assets/{sequenceDiagram-SI44F4Z6-D8Wrx5Vc.js → sequenceDiagram-SI44F4Z6--tnzS2tb.js} +1 -1
  63. package/dist/assets/{sizeCapture-X5ZJPWSS-CXqD68vD.js → sizeCapture-X5ZJPWSS-DujwEOtm.js} +1 -1
  64. package/dist/assets/{stateDiagram-OKZ733FA-CF0Ekqjo.js → stateDiagram-OKZ733FA-BK3TcZY2.js} +1 -1
  65. package/dist/assets/stateDiagram-v2-UEYNNEHI-_medyomK.js +1 -0
  66. package/dist/assets/{swimlanes-SLNWSIFB-DVIax_k4.js → swimlanes-SLNWSIFB-naX-Uv1B.js} +2 -2
  67. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DstIv3TC.js +8 -0
  68. package/dist/assets/{timeline-definition-Z64GVDOM-DSqk3ApG.js → timeline-definition-Z64GVDOM-C-vP_ddU.js} +1 -1
  69. package/dist/assets/{vennDiagram-T6HMQDX7-DNbTqyG5.js → vennDiagram-T6HMQDX7-DN-I7o-7.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-T6FBY63Y-BNCpsFZV.js → wardleyDiagram-T6FBY63Y-Db9dsaXv.js} +1 -1
  71. package/dist/assets/{xychartDiagram-ELKLHX3M-XsVZphHr.js → xychartDiagram-ELKLHX3M-DE6OxW_p.js} +1 -1
  72. package/dist/clear-cache.html +85 -85
  73. package/dist/convert-icons.md +52 -52
  74. package/dist/favicon.svg +8 -8
  75. package/dist/generate-icons.js +48 -48
  76. package/dist/icons/codex-white.svg +3 -3
  77. package/dist/icons/codex.svg +3 -3
  78. package/dist/icons/cursor-white.svg +11 -11
  79. package/dist/icons/icon-128x128.svg +11 -11
  80. package/dist/icons/icon-144x144.svg +11 -11
  81. package/dist/icons/icon-152x152.svg +11 -11
  82. package/dist/icons/icon-192x192.svg +11 -11
  83. package/dist/icons/icon-384x384.svg +11 -11
  84. package/dist/icons/icon-512x512.svg +11 -11
  85. package/dist/icons/icon-72x72.svg +11 -11
  86. package/dist/icons/icon-96x96.svg +11 -11
  87. package/dist/icons/icon-template.svg +11 -11
  88. package/dist/index.html +56 -56
  89. package/dist/logo.svg +17 -17
  90. package/dist/manifest.json +60 -60
  91. package/dist/sw.js +124 -124
  92. package/dist-server/server/modules/cli/cli.service.js +36 -36
  93. package/dist-server/server/modules/cli/sandbox.service.js +38 -38
  94. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +74 -74
  95. package/dist-server/server/modules/commands/commands.routes.js +24 -24
  96. package/dist-server/server/modules/database/migrations.js +190 -190
  97. package/dist-server/server/modules/database/repositories/api-keys.js +3 -3
  98. package/dist-server/server/modules/database/repositories/github-tokens.js +2 -2
  99. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +28 -28
  100. package/dist-server/server/modules/database/repositories/notification-preferences.js +4 -4
  101. package/dist-server/server/modules/database/repositories/projects.db.js +61 -61
  102. package/dist-server/server/modules/database/repositories/provider-models.js +35 -35
  103. package/dist-server/server/modules/database/repositories/push-subscriptions.js +5 -5
  104. package/dist-server/server/modules/database/repositories/scan-state.db.js +5 -5
  105. package/dist-server/server/modules/database/repositories/sessions.db.js +92 -92
  106. package/dist-server/server/modules/database/schema.js +177 -177
  107. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +6 -6
  108. package/dist-server/server/modules/git/git.routes.js +17 -17
  109. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +12 -12
  110. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +9 -9
  111. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +17 -17
  112. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +25 -25
  113. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +27 -27
  114. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +9 -9
  115. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -3
  116. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +96 -96
  117. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +18 -18
  118. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +419 -419
  119. package/electron/cloud.js +260 -260
  120. package/electron/desktopNotifications.js +378 -378
  121. package/electron/desktopWindow.js +766 -766
  122. package/electron/launcher/index.html +14 -14
  123. package/electron/launcher/launcher.css +801 -801
  124. package/electron/launcher/launcher.js +687 -687
  125. package/electron/localServer.js +549 -549
  126. package/electron/main.js +944 -944
  127. package/electron/preload.cjs +60 -60
  128. package/electron/scripts/generate-macos-icon.js +62 -62
  129. package/electron/serverInstaller.js +277 -277
  130. package/electron/tabs.js +87 -87
  131. package/electron/viewHost.js +331 -331
  132. package/package.json +250 -247
  133. package/public/api-docs.html +901 -901
  134. package/scripts/fix-node-pty.js +67 -67
  135. package/scripts/promote-dist-server.mjs +50 -50
  136. package/scripts/release/build-server-bundle.js +176 -176
  137. package/scripts/release/prepare-desktop-app.js +152 -152
  138. package/server/index.ts +399 -399
  139. package/server/load-env.ts +47 -47
  140. package/server/modules/agent/agent.module.ts +52 -52
  141. package/server/modules/agent/agent.routes.ts +1287 -1287
  142. package/server/modules/agent/index.ts +2 -2
  143. package/server/modules/agent/tests/agent.routes.test.ts +205 -205
  144. package/server/modules/assets/assets.routes.ts +150 -150
  145. package/server/modules/assets/index.ts +3 -3
  146. package/server/modules/assets/services/image-assets.service.ts +127 -127
  147. package/server/modules/assets/tests/image-assets.service.test.ts +74 -74
  148. package/server/modules/auth/auth.middleware.ts +163 -163
  149. package/server/modules/auth/auth.module.ts +38 -38
  150. package/server/modules/auth/auth.routes.ts +57 -57
  151. package/server/modules/auth/auth.service.ts +155 -155
  152. package/server/modules/auth/index.ts +9 -9
  153. package/server/modules/auth/tests/auth.service.test.ts +95 -95
  154. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -120
  155. package/server/modules/browser-use/browser-use-mcp.ts +386 -386
  156. package/server/modules/browser-use/browser-use-runtime.ts +10 -10
  157. package/server/modules/browser-use/browser-use.routes.ts +96 -96
  158. package/server/modules/browser-use/browser-use.service.ts +836 -836
  159. package/server/modules/browser-use/index.ts +11 -11
  160. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -10
  161. package/server/modules/cli/cli.module.ts +93 -93
  162. package/server/modules/cli/cli.service.ts +248 -248
  163. package/server/modules/cli/cli.ts +19 -19
  164. package/server/modules/cli/index.ts +2 -2
  165. package/server/modules/cli/sandbox.service.ts +398 -398
  166. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -186
  167. package/server/modules/cli/tests/cli.service.test.ts +86 -86
  168. package/server/modules/cli/tests/sandbox.service.test.ts +58 -58
  169. package/server/modules/commands/commands.module.ts +22 -22
  170. package/server/modules/commands/commands.routes.ts +586 -586
  171. package/server/modules/commands/index.ts +2 -2
  172. package/server/modules/commands/tests/commands.test.ts +109 -109
  173. package/server/modules/database/connection.ts +143 -143
  174. package/server/modules/database/index.ts +17 -17
  175. package/server/modules/database/init-db.ts +17 -17
  176. package/server/modules/database/migrations.ts +515 -515
  177. package/server/modules/database/repositories/api-keys.ts +119 -119
  178. package/server/modules/database/repositories/app-config.ts +53 -53
  179. package/server/modules/database/repositories/credentials.ts +106 -106
  180. package/server/modules/database/repositories/github-tokens.ts +100 -100
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -153
  182. package/server/modules/database/repositories/notification-preferences.ts +117 -117
  183. package/server/modules/database/repositories/projects.db.ts +196 -196
  184. package/server/modules/database/repositories/provider-models.ts +168 -168
  185. package/server/modules/database/repositories/push-subscriptions.ts +80 -80
  186. package/server/modules/database/repositories/scan-state.db.ts +42 -42
  187. package/server/modules/database/repositories/sessions.db.ts +524 -524
  188. package/server/modules/database/repositories/users.ts +140 -140
  189. package/server/modules/database/repositories/vapid-keys.ts +57 -57
  190. package/server/modules/database/schema.ts +210 -210
  191. package/server/modules/database/tests/projects.db.integration.test.ts +72 -72
  192. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -125
  193. package/server/modules/database/tests/sessions-provider-mapping.test.ts +109 -109
  194. package/server/modules/database/tests/sessions.db.integration.test.ts +116 -116
  195. package/server/modules/file-tree/file-tree.module.ts +116 -116
  196. package/server/modules/file-tree/file-tree.routes.ts +258 -258
  197. package/server/modules/file-tree/file-tree.service.ts +675 -675
  198. package/server/modules/file-tree/index.ts +2 -2
  199. package/server/modules/file-tree/tests/file-tree.routes.test.ts +157 -157
  200. package/server/modules/file-tree/tests/file-tree.service.test.ts +371 -371
  201. package/server/modules/git/git-branch.service.ts +43 -43
  202. package/server/modules/git/git-parsing.service.ts +66 -66
  203. package/server/modules/git/git.module.ts +22 -22
  204. package/server/modules/git/git.routes.ts +1596 -1596
  205. package/server/modules/git/index.ts +2 -2
  206. package/server/modules/git/tests/git-branch.service.test.ts +60 -60
  207. package/server/modules/git/tests/git-init.routes.test.ts +132 -132
  208. package/server/modules/git/tests/git.test.ts +106 -106
  209. package/server/modules/notifications/index.ts +24 -24
  210. package/server/modules/notifications/notifications.routes.ts +127 -127
  211. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -124
  212. package/server/modules/notifications/services/notification-orchestrator.service.js +310 -310
  213. package/server/modules/notifications/tests/notification-orchestrator.integration.test.ts +55 -55
  214. package/server/modules/notifications/vapid-keys.service.ts +38 -38
  215. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -109
  216. package/server/modules/plugins/index.ts +9 -9
  217. package/server/modules/plugins/plugin-process.service.ts +217 -217
  218. package/server/modules/plugins/plugin-registry.service.ts +459 -459
  219. package/server/modules/plugins/plugins.module.ts +33 -33
  220. package/server/modules/plugins/plugins.routes.ts +66 -66
  221. package/server/modules/plugins/plugins.service.ts +147 -147
  222. package/server/modules/plugins/tests/plugins.service.test.ts +32 -32
  223. package/server/modules/projects/index.ts +11 -11
  224. package/server/modules/projects/projects.routes.ts +273 -273
  225. package/server/modules/projects/services/project-clone.service.ts +323 -323
  226. package/server/modules/projects/services/project-delete.service.ts +90 -90
  227. package/server/modules/projects/services/project-management.service.ts +144 -144
  228. package/server/modules/projects/services/project-star.service.ts +78 -78
  229. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -248
  230. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +314 -314
  231. package/server/modules/projects/tests/project-clone.service.test.ts +183 -183
  232. package/server/modules/projects/tests/project-management.service.test.ts +117 -117
  233. package/server/modules/projects/tests/project-star.service.test.ts +123 -123
  234. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -105
  235. package/server/modules/providers/README.md +380 -380
  236. package/server/modules/providers/index.ts +10 -10
  237. package/server/modules/providers/list/claude/claude-auth.provider.ts +160 -160
  238. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  239. package/server/modules/providers/list/claude/claude-models.provider.ts +294 -294
  240. package/server/modules/providers/list/claude/claude-runtime.provider.js +1068 -1068
  241. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +203 -203
  242. package/server/modules/providers/list/claude/claude-sessions.provider.ts +684 -684
  243. package/server/modules/providers/list/claude/claude-skills.provider.ts +265 -265
  244. package/server/modules/providers/list/claude/claude.provider.ts +30 -30
  245. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -100
  246. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  247. package/server/modules/providers/list/codex/codex-models.provider.ts +123 -123
  248. package/server/modules/providers/list/codex/codex-runtime.provider.js +533 -533
  249. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +203 -203
  250. package/server/modules/providers/list/codex/codex-sessions.provider.ts +938 -938
  251. package/server/modules/providers/list/codex/codex-skills.provider.ts +73 -73
  252. package/server/modules/providers/list/codex/codex.provider.ts +28 -28
  253. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  254. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  255. package/server/modules/providers/list/cursor/cursor-models.provider.ts +119 -119
  256. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +385 -385
  257. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +159 -159
  258. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +654 -654
  259. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +39 -39
  260. package/server/modules/providers/list/cursor/cursor.provider.ts +30 -30
  261. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +110 -110
  262. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -228
  263. package/server/modules/providers/list/opencode/opencode-models.provider.ts +203 -203
  264. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +434 -434
  265. package/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +261 -261
  266. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +179 -179
  267. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +500 -500
  268. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -78
  269. package/server/modules/providers/list/opencode/opencode.provider.ts +30 -30
  270. package/server/modules/providers/provider.registry.ts +36 -36
  271. package/server/modules/providers/provider.routes.ts +877 -877
  272. package/server/modules/providers/services/mcp.service.ts +109 -109
  273. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  274. package/server/modules/providers/services/provider-capabilities.service.ts +97 -97
  275. package/server/modules/providers/services/provider-models.service.ts +401 -401
  276. package/server/modules/providers/services/provider-runtime.service.ts +108 -108
  277. package/server/modules/providers/services/provider-token-usage.service.ts +357 -357
  278. package/server/modules/providers/services/session-conversations-search.service.ts +1321 -1321
  279. package/server/modules/providers/services/session-synchronizer.service.ts +74 -74
  280. package/server/modules/providers/services/sessions-watcher.service.ts +247 -247
  281. package/server/modules/providers/services/sessions.service.ts +471 -471
  282. package/server/modules/providers/services/skills.service.ts +39 -39
  283. package/server/modules/providers/shared/base/abstract.provider.ts +32 -32
  284. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  285. package/server/modules/providers/shared/skills/skills.provider.ts +287 -287
  286. package/server/modules/providers/tests/claude-auth.test.ts +150 -150
  287. package/server/modules/providers/tests/claude-sessions.test.ts +61 -61
  288. package/server/modules/providers/tests/codex-sessions.test.ts +231 -231
  289. package/server/modules/providers/tests/mcp.test.ts +349 -349
  290. package/server/modules/providers/tests/opencode-models.test.ts +41 -41
  291. package/server/modules/providers/tests/opencode-sessions.test.ts +522 -522
  292. package/server/modules/providers/tests/provider-attachment-history.test.ts +247 -247
  293. package/server/modules/providers/tests/provider-models.service.test.ts +353 -353
  294. package/server/modules/providers/tests/provider-runtime.service.test.ts +138 -138
  295. package/server/modules/providers/tests/provider-token-usage.service.test.ts +186 -186
  296. package/server/modules/providers/tests/provider.routes.test.ts +244 -244
  297. package/server/modules/providers/tests/sessions-details.test.ts +73 -73
  298. package/server/modules/providers/tests/sessions.service.test.ts +127 -127
  299. package/server/modules/providers/tests/skills.test.ts +692 -692
  300. package/server/modules/settings/index.ts +2 -2
  301. package/server/modules/settings/settings.module.ts +50 -50
  302. package/server/modules/settings/settings.routes.ts +51 -51
  303. package/server/modules/settings/settings.service.ts +186 -186
  304. package/server/modules/settings/tests/settings.service.test.ts +54 -54
  305. package/server/modules/system/index.ts +2 -2
  306. package/server/modules/system/system.module.ts +62 -62
  307. package/server/modules/system/system.routes.ts +21 -21
  308. package/server/modules/system/system.service.ts +76 -76
  309. package/server/modules/system/tests/system.service.test.ts +118 -118
  310. package/server/modules/taskmaster/index.ts +2 -2
  311. package/server/modules/taskmaster/taskmaster.module.ts +24 -24
  312. package/server/modules/taskmaster/taskmaster.routes.ts +1482 -1482
  313. package/server/modules/taskmaster/taskmaster.service.ts +132 -132
  314. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +144 -144
  315. package/server/modules/taskmaster/tests/taskmaster.service.test.ts +97 -97
  316. package/server/modules/user/index.ts +2 -2
  317. package/server/modules/user/tests/user.service.test.ts +61 -61
  318. package/server/modules/user/user.module.ts +58 -58
  319. package/server/modules/user/user.routes.ts +50 -50
  320. package/server/modules/user/user.service.ts +85 -85
  321. package/server/modules/voice/index.ts +2 -2
  322. package/server/modules/voice/tests/voice.service.test.ts +98 -98
  323. package/server/modules/voice/voice.module.ts +47 -47
  324. package/server/modules/voice/voice.routes.ts +113 -113
  325. package/server/modules/voice/voice.service.ts +192 -192
  326. package/server/modules/websocket/README.md +273 -273
  327. package/server/modules/websocket/index.ts +3 -3
  328. package/server/modules/websocket/services/chat-run-registry.service.ts +343 -343
  329. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -145
  330. package/server/modules/websocket/services/chat-websocket.service.ts +425 -425
  331. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -65
  332. package/server/modules/websocket/services/shell-websocket.service.ts +502 -502
  333. package/server/modules/websocket/services/websocket-auth.service.ts +59 -59
  334. package/server/modules/websocket/services/websocket-server.service.ts +124 -124
  335. package/server/modules/websocket/services/websocket-state.service.ts +16 -16
  336. package/server/modules/websocket/services/websocket-writer.service.ts +38 -38
  337. package/server/modules/websocket/tests/chat-attachment-filter.test.ts +59 -59
  338. package/server/modules/websocket/tests/chat-run-registry.test.ts +282 -282
  339. package/server/modules/websocket/tests/shell-websocket.service.test.ts +100 -100
  340. package/server/modules/websocket/tests/websocket-heartbeat.service.test.ts +78 -78
  341. package/server/modules/worktrees/index.ts +2 -2
  342. package/server/modules/worktrees/services/worktree-create-and-open.service.ts +53 -53
  343. package/server/modules/worktrees/services/worktree-create.service.ts +104 -104
  344. package/server/modules/worktrees/services/worktree-git.service.ts +207 -207
  345. package/server/modules/worktrees/services/worktree-list.service.ts +143 -143
  346. package/server/modules/worktrees/services/worktree-merge.service.ts +157 -157
  347. package/server/modules/worktrees/services/worktree-open.service.ts +85 -85
  348. package/server/modules/worktrees/services/worktree-remove.service.ts +88 -88
  349. package/server/modules/worktrees/tests/worktree-create-and-open.service.test.ts +68 -68
  350. package/server/modules/worktrees/tests/worktree-create.service.test.ts +142 -142
  351. package/server/modules/worktrees/tests/worktree-git.service.test.ts +84 -84
  352. package/server/modules/worktrees/tests/worktree-list.service.test.ts +45 -45
  353. package/server/modules/worktrees/tests/worktree-merge.service.test.ts +213 -213
  354. package/server/modules/worktrees/tests/worktree-open.service.test.ts +122 -122
  355. package/server/modules/worktrees/tests/worktree-remove.service.test.ts +227 -227
  356. package/server/modules/worktrees/tests/worktrees.routes.test.ts +199 -199
  357. package/server/modules/worktrees/worktrees.module.ts +113 -113
  358. package/server/modules/worktrees/worktrees.routes.ts +116 -116
  359. package/server/shared/claude-cli-path.ts +139 -139
  360. package/server/shared/frontmatter.ts +18 -18
  361. package/server/shared/image-attachments.ts +435 -435
  362. package/server/shared/interfaces.ts +179 -179
  363. package/server/shared/tests/claude-cli-path.test.ts +61 -61
  364. package/server/shared/tests/image-attachments.test.ts +353 -353
  365. package/server/shared/tests/slice-tail-page.test.ts +42 -42
  366. package/server/shared/types.ts +1226 -1226
  367. package/server/shared/utils.ts +1161 -1161
  368. package/server/tsconfig.json +40 -40
  369. package/shared/networkHosts.js +22 -22
  370. package/dist/assets/channel-Bm0ZLE4z.js +0 -1
  371. package/dist/assets/classDiagram-JCYQIIEL-DqEOcNxw.js +0 -1
  372. package/dist/assets/classDiagram-v2-OCEON4UE-DqEOcNxw.js +0 -1
  373. package/dist/assets/stateDiagram-v2-UEYNNEHI-DARLpEFJ.js +0 -1
  374. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CrgDgaY8.js +0 -8
@@ -1,435 +1,435 @@
1
- import { promises as fs, realpathSync } from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- /**
6
- * Shared chat-attachment plumbing for every provider runtime.
7
- *
8
- * Uploaded chat files are persisted once in the global `~/.cloudcli/assets`
9
- * folder and referenced by absolute path everywhere else:
10
- * - Claude: paths are read back into base64 `image` content blocks.
11
- * - Codex: paths become `local_image` input items.
12
- * - General files: verified paths are appended inside a `<files_input>` tag,
13
- * which every provider history adapter strips back out for display.
14
- * - Cursor/OpenCode images: paths use the equivalent `<images_input>` tag.
15
- *
16
- * The chat UI loads them through dedicated `/api/assets/images/:filename` and
17
- * `/api/assets/files/:filename` routes, which serve only from this folder.
18
- */
19
-
20
- /** Global storage folder for uploaded chat image attachments. */
21
- export function getGlobalImageAssetsDir(): string {
22
- return path.join(os.homedir(), '.cloudcli', 'assets');
23
- }
24
-
25
- export type ImageAttachmentDescriptor = {
26
- /** Project-relative (preferred) or absolute path to the stored image. */
27
- path: string;
28
- name?: string;
29
- mimeType?: string;
30
- size?: number;
31
- };
32
-
33
- /** Provider-neutral descriptor used for both image and non-image chat attachments. */
34
- export type ChatAttachmentDescriptor = ImageAttachmentDescriptor;
35
-
36
- /** Media types the Claude Messages API accepts for base64 image blocks. */
37
- const CLAUDE_IMAGE_MEDIA_TYPES = new Set([
38
- 'image/jpeg',
39
- 'image/png',
40
- 'image/gif',
41
- 'image/webp',
42
- ]);
43
-
44
- const EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
45
- '.jpg': 'image/jpeg',
46
- '.jpeg': 'image/jpeg',
47
- '.png': 'image/png',
48
- '.gif': 'image/gif',
49
- '.webp': 'image/webp',
50
- '.svg': 'image/svg+xml',
51
- };
52
-
53
- /**
54
- * Accepts a loosely typed chat attachment payload and returns only well-formed
55
- * descriptors. The websocket gateway and provider adapters use this to handle
56
- * current `attachments` payloads and legacy image path arrays consistently.
57
- */
58
- export function normalizeAttachmentDescriptors(attachments: unknown): ChatAttachmentDescriptor[] {
59
- if (!Array.isArray(attachments)) {
60
- return [];
61
- }
62
-
63
- const descriptors: ChatAttachmentDescriptor[] = [];
64
- for (const entry of attachments) {
65
- if (typeof entry === 'string' && entry.trim()) {
66
- descriptors.push({ path: entry.trim() });
67
- continue;
68
- }
69
- if (entry && typeof entry === 'object') {
70
- const record = entry as Record<string, unknown>;
71
- const entryPath = typeof record.path === 'string' ? record.path.trim() : '';
72
- if (!entryPath) {
73
- continue;
74
- }
75
- const descriptor: ChatAttachmentDescriptor = { path: entryPath };
76
- if (typeof record.name === 'string') {
77
- descriptor.name = record.name;
78
- }
79
- if (typeof record.mimeType === 'string') {
80
- descriptor.mimeType = record.mimeType;
81
- }
82
- if (typeof record.size === 'number' && Number.isFinite(record.size)) {
83
- descriptor.size = record.size;
84
- }
85
- descriptors.push(descriptor);
86
- }
87
- }
88
- return descriptors;
89
- }
90
-
91
- /** Backward-compatible image-specific alias used by existing provider adapters. */
92
- export function normalizeImageDescriptors(images: unknown): ImageAttachmentDescriptor[] {
93
- return normalizeAttachmentDescriptors(images);
94
- }
95
-
96
- const IMAGE_FILE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
97
-
98
- /**
99
- * Determines whether the websocket gateway should route an attachment through
100
- * provider-native image input instead of the general file reference channel.
101
- */
102
- export function isImageAttachmentDescriptor(descriptor: ChatAttachmentDescriptor): boolean {
103
- if (descriptor.mimeType && CLAUDE_IMAGE_MEDIA_TYPES.has(descriptor.mimeType)) {
104
- return true;
105
- }
106
- return IMAGE_FILE_EXTENSIONS.has(path.extname(descriptor.path).toLowerCase());
107
- }
108
-
109
- /** Normalizes Windows separators so stored references stay portable. */
110
- export function toPosixPath(value: string): string {
111
- return value.replace(/\\/g, '/');
112
- }
113
-
114
- /** Resolves a project-relative image path against the run's working directory. */
115
- export function resolveImageAbsolutePath(cwd: string | undefined, imagePath: string): string {
116
- if (path.isAbsolute(imagePath)) {
117
- return imagePath;
118
- }
119
- return path.resolve(cwd || process.cwd(), imagePath);
120
- }
121
-
122
- function isPathInsideDirectory(candidate: string, directory: string): boolean {
123
- // resolve + startsWith(root + separator) is the containment idiom CodeQL
124
- // recognizes as a path-injection barrier, and matches the check used by
125
- // resolveImageAssetFile in the assets module. The root itself never
126
- // matches (no trailing separator after resolve), only entries below it.
127
- const resolvedRoot = path.resolve(directory) + path.sep;
128
- return path.resolve(candidate).startsWith(resolvedRoot);
129
- }
130
-
131
- function getDirectoryPathVariants(directory: string): string[] {
132
- const resolvedDirectory = path.resolve(directory);
133
- try {
134
- const canonicalDirectory = path.resolve(realpathSync(directory));
135
- return canonicalDirectory === resolvedDirectory
136
- ? [resolvedDirectory]
137
- : [resolvedDirectory, canonicalDirectory];
138
- } catch {
139
- return [resolvedDirectory];
140
- }
141
- }
142
-
143
- /**
144
- * Second layer of the image trust boundary (the first is the chat.send filter
145
- * in the websocket gateway): provider builders only reference files that live
146
- * in the global upload store or inside the run's working directory — places
147
- * the agent could already access on its own. Anything else (e.g. `~/.ssh`) is
148
- * refused, so a caller-supplied descriptor can never leak arbitrary files.
149
- */
150
- export function isAllowedImageSourcePath(resolvedPath: string, cwd?: string): boolean {
151
- return [getGlobalImageAssetsDir(), cwd || process.cwd()].some((directory) =>
152
- getDirectoryPathVariants(directory).some((directoryVariant) =>
153
- isPathInsideDirectory(resolvedPath, directoryVariant)
154
- )
155
- );
156
- }
157
-
158
- /**
159
- * Resolves the media type for one image, preferring the uploaded mime type and
160
- * falling back to the file extension.
161
- */
162
- export function resolveImageMediaType(descriptor: ImageAttachmentDescriptor): string | null {
163
- if (descriptor.mimeType) {
164
- return descriptor.mimeType;
165
- }
166
- const extension = path.extname(descriptor.path).toLowerCase();
167
- return EXTENSION_TO_MEDIA_TYPE[extension] || null;
168
- }
169
-
170
- const IMAGES_INPUT_TAG_PATTERN = /\s*<images_input>([\s\S]*?)<\/images_input>\s*/g;
171
-
172
- // One image reference recovered from an <images_input> block: the stored
173
- // asset path plus the user's original filename when it was recorded.
174
- export type ParsedImageAttachment = {
175
- path: string;
176
- name?: string;
177
- };
178
-
179
- // Result of stripping an <images_input> block out of persisted prompt text.
180
- // `imagePaths` mirrors `attachments` for callers that only need paths.
181
- export type ParsedImagesInput = {
182
- text: string;
183
- imagePaths: string[];
184
- attachments: ParsedImageAttachment[];
185
- };
186
-
187
- /**
188
- * Appends the `<images_input>` reference block used by the Cursor and
189
- * OpenCode CLIs. The block carries one numbered line per attachment with
190
- * the stored file path (quote-free on purpose — Windows .cmd shims mangle
191
- * quoted text) and the user's original filename, plus an explicit instruction
192
- * to read the files and keep the block out of the reply. The same block is
193
- * stripped back out of persisted history by {@link parseImagesInputTag}.
194
- */
195
- export function appendImagesInputTag(prompt: string, images: unknown): string {
196
- const descriptors = normalizeImageDescriptors(images);
197
- if (descriptors.length === 0) {
198
- return prompt;
199
- }
200
-
201
- const entryLines = descriptors.map((descriptor, index) => {
202
- const entryPath = toPosixPath(descriptor.path);
203
- // Parentheses and newlines would break the "(original name: ...)" suffix
204
- // the parser looks for, so drop them from the display name.
205
- const cleanName = descriptor.name?.replace(/[()\r\n]/g, '').trim();
206
- return cleanName
207
- ? `${index + 1}. ${entryPath} (original name: ${cleanName})`
208
- : `${index + 1}. ${entryPath}`;
209
- });
210
-
211
- return [
212
- prompt,
213
- '',
214
- '<images_input>',
215
- `The user attached ${descriptors.length} image(s) to this message. Read each file listed below with your file/image reading tool and use what you see to answer the prompt above. Respond as if the images were attached directly. Do not mention this block or the file paths unless the user asks about them.`,
216
- ...entryLines,
217
- '</images_input>',
218
- ].join('\n');
219
- }
220
-
221
- const FILES_INPUT_TAG_PATTERN = /\s*<files_input>([\s\S]*?)<\/files_input>\s*/g;
222
-
223
- /**
224
- * Appends a provider-neutral file reference block to a prompt. Provider agents
225
- * receive only server-validated paths from the global attachment store and can
226
- * inspect each file with their normal filesystem tools.
227
- */
228
- export function appendFilesInputTag(prompt: string, files: unknown): string {
229
- const descriptors = normalizeAttachmentDescriptors(files);
230
- if (descriptors.length === 0) {
231
- return prompt;
232
- }
233
-
234
- const entryLines = descriptors.map((descriptor, index) => {
235
- const entryPath = toPosixPath(descriptor.path);
236
- const cleanName = descriptor.name?.replace(/[()\r\n]/g, '').trim();
237
- return cleanName
238
- ? `${index + 1}. ${entryPath} (original name: ${cleanName})`
239
- : `${index + 1}. ${entryPath}`;
240
- });
241
-
242
- return [
243
- prompt,
244
- '',
245
- '<files_input>',
246
- `The user attached ${descriptors.length} file(s) to this message. Read each file listed below with your file reading tools and use its contents to answer the prompt above. Do not mention this block or the file paths unless the user asks about them.`,
247
- ...entryLines,
248
- '</files_input>',
249
- ].join('\n');
250
- }
251
-
252
- // Matches one numbered attachment entry inside the tag body. Works for both
253
- // the multi-line block and the Windows-flattened single-line form, where the
254
- // next ` N. ` marker (or the end of the body) delimits each entry.
255
- const IMAGES_INPUT_ENTRY_PATTERN = /\d+\.\s+(.+?)(?=\s+\d+\.\s+|\s*$)/g;
256
-
257
- const ORIGINAL_NAME_SUFFIX_PATTERN = /\(original name: ([^)]*)\)\s*$/;
258
-
259
- function parseNumberedImageEntries(inner: string): ParsedImageAttachment[] {
260
- const attachments: ParsedImageAttachment[] = [];
261
- for (const entryMatch of inner.matchAll(IMAGES_INPUT_ENTRY_PATTERN)) {
262
- let entryText = entryMatch[1].trim();
263
- let name: string | undefined;
264
-
265
- const nameMatch = ORIGINAL_NAME_SUFFIX_PATTERN.exec(entryText);
266
- if (nameMatch) {
267
- name = nameMatch[1].trim() || undefined;
268
- entryText = entryText.slice(0, nameMatch.index).trim();
269
- }
270
-
271
- if (entryText) {
272
- attachments.push(name ? { path: toPosixPath(entryText), name } : { path: toPosixPath(entryText) });
273
- }
274
- }
275
- return attachments;
276
- }
277
-
278
- /**
279
- * Strips the last provider-neutral file reference block from persisted prompt
280
- * text and restores its attachment descriptors for chat history.
281
- */
282
- export function parseFilesInputTag(text: string): {
283
- text: string;
284
- filePaths: string[];
285
- attachments: ParsedImageAttachment[];
286
- } {
287
- if (typeof text !== 'string' || !text.includes('<files_input>')) {
288
- return { text, filePaths: [], attachments: [] };
289
- }
290
-
291
- let lastMatch: RegExpExecArray | null = null;
292
- FILES_INPUT_TAG_PATTERN.lastIndex = 0;
293
- for (let match = FILES_INPUT_TAG_PATTERN.exec(text); match; match = FILES_INPUT_TAG_PATTERN.exec(text)) {
294
- lastMatch = match;
295
- }
296
- if (!lastMatch) {
297
- return { text, filePaths: [], attachments: [] };
298
- }
299
-
300
- const attachments = parseNumberedImageEntries(lastMatch[1]);
301
- const stripped = (
302
- text.slice(0, lastMatch.index) + '\n' + text.slice(lastMatch.index + lastMatch[0].length)
303
- ).trim();
304
-
305
- return {
306
- text: stripped,
307
- filePaths: attachments.map((attachment) => attachment.path),
308
- attachments,
309
- };
310
- }
311
-
312
- /**
313
- * Strips one `<images_input>` block from persisted prompt text and returns
314
- * the clean text plus the referenced attachments (path and original name).
315
- *
316
- * Only the LAST block in the text is treated as the attachment carrier — the
317
- * composer always appends it at the end, so a user who literally typed
318
- * `<images_input>` earlier in their prompt keeps that text intact.
319
- *
320
- * Understands the numbered-line body in both its multi-line and
321
- * Windows-flattened single-line forms.
322
- */
323
- export function parseImagesInputTag(text: string): ParsedImagesInput {
324
- if (typeof text !== 'string' || !text.includes('<images_input>')) {
325
- return { text, imagePaths: [], attachments: [] };
326
- }
327
-
328
- let lastMatch: RegExpExecArray | null = null;
329
- IMAGES_INPUT_TAG_PATTERN.lastIndex = 0;
330
- for (let match = IMAGES_INPUT_TAG_PATTERN.exec(text); match; match = IMAGES_INPUT_TAG_PATTERN.exec(text)) {
331
- lastMatch = match;
332
- }
333
- if (!lastMatch) {
334
- return { text, imagePaths: [], attachments: [] };
335
- }
336
-
337
- const attachments = parseNumberedImageEntries(lastMatch[1]);
338
-
339
- const stripped = (
340
- text.slice(0, lastMatch.index) + '\n' + text.slice(lastMatch.index + lastMatch[0].length)
341
- ).trim();
342
-
343
- return {
344
- text: stripped,
345
- imagePaths: attachments.map((attachment) => attachment.path),
346
- attachments,
347
- };
348
- }
349
-
350
- /** Maps raw image paths to the attachment shape carried by NormalizedMessage.images. */
351
- export function toImageAttachments(imagePaths: string[]): Array<{ path: string }> {
352
- return imagePaths.map((imagePath) => ({ path: toPosixPath(imagePath) }));
353
- }
354
-
355
- type ClaudeContentBlock =
356
- | { type: 'text'; text: string }
357
- | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
358
-
359
- /**
360
- * Builds the Claude user-message content list: the prompt text followed by one
361
- * base64 `image` block per attachment. Images the Claude API cannot accept
362
- * (e.g. SVG) or that fail to read are skipped with a warning so the prompt
363
- * itself still goes through.
364
- */
365
- export async function buildClaudeUserContent(
366
- prompt: string,
367
- images: unknown,
368
- cwd?: string,
369
- ): Promise<ClaudeContentBlock[]> {
370
- const blocks: ClaudeContentBlock[] = [{ type: 'text', text: prompt }];
371
-
372
- for (const descriptor of normalizeImageDescriptors(images)) {
373
- const mediaType = resolveImageMediaType(descriptor);
374
- if (!mediaType || !CLAUDE_IMAGE_MEDIA_TYPES.has(mediaType)) {
375
- console.warn(`[Images] Skipping unsupported Claude image type for ${descriptor.path}`);
376
- continue;
377
- }
378
-
379
- const resolvedPath = resolveImageAbsolutePath(cwd, descriptor.path);
380
- if (!isAllowedImageSourcePath(resolvedPath, cwd)) {
381
- console.warn(`[Images] Refusing to read image outside allowed roots: ${descriptor.path}`);
382
- continue;
383
- }
384
-
385
- try {
386
- const canonicalPath = await fs.realpath(resolvedPath);
387
- if (!isAllowedImageSourcePath(canonicalPath, cwd)) {
388
- console.warn(`[Images] Refusing to read symlinked image outside allowed roots: ${descriptor.path}`);
389
- continue;
390
- }
391
-
392
- const bytes = await fs.readFile(canonicalPath);
393
- blocks.push({
394
- type: 'image',
395
- source: {
396
- type: 'base64',
397
- media_type: mediaType,
398
- data: bytes.toString('base64'),
399
- },
400
- });
401
- } catch (error) {
402
- const message = error instanceof Error ? error.message : String(error);
403
- console.warn(`[Images] Failed to read image ${descriptor.path}: ${message}`);
404
- }
405
- }
406
-
407
- return blocks;
408
- }
409
-
410
- type CodexInputItem =
411
- | { type: 'text'; text: string }
412
- | { type: 'local_image'; path: string };
413
-
414
- /**
415
- * Builds the Codex `runStreamed` input list: prompt text plus one
416
- * `local_image` item per attachment, resolved to absolute paths so the Codex
417
- * runtime can read them regardless of its own working directory handling.
418
- */
419
- export function buildCodexInputItems(prompt: string, images: unknown, cwd?: string): CodexInputItem[] {
420
- const items: CodexInputItem[] = [{ type: 'text', text: prompt }];
421
- for (const descriptor of normalizeImageDescriptors(images)) {
422
- const resolvedPath = resolveImageAbsolutePath(cwd, descriptor.path);
423
- if (!isAllowedImageSourcePath(resolvedPath, cwd)) {
424
- // Same trust boundary as buildClaudeUserContent — the Codex runtime
425
- // reads this file, so it must stay within the allowed roots.
426
- console.warn(`[Images] Refusing to attach image outside allowed roots: ${descriptor.path}`);
427
- continue;
428
- }
429
- items.push({
430
- type: 'local_image',
431
- path: resolvedPath,
432
- });
433
- }
434
- return items;
435
- }
1
+ import { promises as fs, realpathSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Shared chat-attachment plumbing for every provider runtime.
7
+ *
8
+ * Uploaded chat files are persisted once in the global `~/.cloudcli/assets`
9
+ * folder and referenced by absolute path everywhere else:
10
+ * - Claude: paths are read back into base64 `image` content blocks.
11
+ * - Codex: paths become `local_image` input items.
12
+ * - General files: verified paths are appended inside a `<files_input>` tag,
13
+ * which every provider history adapter strips back out for display.
14
+ * - Cursor/OpenCode images: paths use the equivalent `<images_input>` tag.
15
+ *
16
+ * The chat UI loads them through dedicated `/api/assets/images/:filename` and
17
+ * `/api/assets/files/:filename` routes, which serve only from this folder.
18
+ */
19
+
20
+ /** Global storage folder for uploaded chat image attachments. */
21
+ export function getGlobalImageAssetsDir(): string {
22
+ return path.join(os.homedir(), '.cloudcli', 'assets');
23
+ }
24
+
25
+ export type ImageAttachmentDescriptor = {
26
+ /** Project-relative (preferred) or absolute path to the stored image. */
27
+ path: string;
28
+ name?: string;
29
+ mimeType?: string;
30
+ size?: number;
31
+ };
32
+
33
+ /** Provider-neutral descriptor used for both image and non-image chat attachments. */
34
+ export type ChatAttachmentDescriptor = ImageAttachmentDescriptor;
35
+
36
+ /** Media types the Claude Messages API accepts for base64 image blocks. */
37
+ const CLAUDE_IMAGE_MEDIA_TYPES = new Set([
38
+ 'image/jpeg',
39
+ 'image/png',
40
+ 'image/gif',
41
+ 'image/webp',
42
+ ]);
43
+
44
+ const EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
45
+ '.jpg': 'image/jpeg',
46
+ '.jpeg': 'image/jpeg',
47
+ '.png': 'image/png',
48
+ '.gif': 'image/gif',
49
+ '.webp': 'image/webp',
50
+ '.svg': 'image/svg+xml',
51
+ };
52
+
53
+ /**
54
+ * Accepts a loosely typed chat attachment payload and returns only well-formed
55
+ * descriptors. The websocket gateway and provider adapters use this to handle
56
+ * current `attachments` payloads and legacy image path arrays consistently.
57
+ */
58
+ export function normalizeAttachmentDescriptors(attachments: unknown): ChatAttachmentDescriptor[] {
59
+ if (!Array.isArray(attachments)) {
60
+ return [];
61
+ }
62
+
63
+ const descriptors: ChatAttachmentDescriptor[] = [];
64
+ for (const entry of attachments) {
65
+ if (typeof entry === 'string' && entry.trim()) {
66
+ descriptors.push({ path: entry.trim() });
67
+ continue;
68
+ }
69
+ if (entry && typeof entry === 'object') {
70
+ const record = entry as Record<string, unknown>;
71
+ const entryPath = typeof record.path === 'string' ? record.path.trim() : '';
72
+ if (!entryPath) {
73
+ continue;
74
+ }
75
+ const descriptor: ChatAttachmentDescriptor = { path: entryPath };
76
+ if (typeof record.name === 'string') {
77
+ descriptor.name = record.name;
78
+ }
79
+ if (typeof record.mimeType === 'string') {
80
+ descriptor.mimeType = record.mimeType;
81
+ }
82
+ if (typeof record.size === 'number' && Number.isFinite(record.size)) {
83
+ descriptor.size = record.size;
84
+ }
85
+ descriptors.push(descriptor);
86
+ }
87
+ }
88
+ return descriptors;
89
+ }
90
+
91
+ /** Backward-compatible image-specific alias used by existing provider adapters. */
92
+ export function normalizeImageDescriptors(images: unknown): ImageAttachmentDescriptor[] {
93
+ return normalizeAttachmentDescriptors(images);
94
+ }
95
+
96
+ const IMAGE_FILE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
97
+
98
+ /**
99
+ * Determines whether the websocket gateway should route an attachment through
100
+ * provider-native image input instead of the general file reference channel.
101
+ */
102
+ export function isImageAttachmentDescriptor(descriptor: ChatAttachmentDescriptor): boolean {
103
+ if (descriptor.mimeType && CLAUDE_IMAGE_MEDIA_TYPES.has(descriptor.mimeType)) {
104
+ return true;
105
+ }
106
+ return IMAGE_FILE_EXTENSIONS.has(path.extname(descriptor.path).toLowerCase());
107
+ }
108
+
109
+ /** Normalizes Windows separators so stored references stay portable. */
110
+ export function toPosixPath(value: string): string {
111
+ return value.replace(/\\/g, '/');
112
+ }
113
+
114
+ /** Resolves a project-relative image path against the run's working directory. */
115
+ export function resolveImageAbsolutePath(cwd: string | undefined, imagePath: string): string {
116
+ if (path.isAbsolute(imagePath)) {
117
+ return imagePath;
118
+ }
119
+ return path.resolve(cwd || process.cwd(), imagePath);
120
+ }
121
+
122
+ function isPathInsideDirectory(candidate: string, directory: string): boolean {
123
+ // resolve + startsWith(root + separator) is the containment idiom CodeQL
124
+ // recognizes as a path-injection barrier, and matches the check used by
125
+ // resolveImageAssetFile in the assets module. The root itself never
126
+ // matches (no trailing separator after resolve), only entries below it.
127
+ const resolvedRoot = path.resolve(directory) + path.sep;
128
+ return path.resolve(candidate).startsWith(resolvedRoot);
129
+ }
130
+
131
+ function getDirectoryPathVariants(directory: string): string[] {
132
+ const resolvedDirectory = path.resolve(directory);
133
+ try {
134
+ const canonicalDirectory = path.resolve(realpathSync(directory));
135
+ return canonicalDirectory === resolvedDirectory
136
+ ? [resolvedDirectory]
137
+ : [resolvedDirectory, canonicalDirectory];
138
+ } catch {
139
+ return [resolvedDirectory];
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Second layer of the image trust boundary (the first is the chat.send filter
145
+ * in the websocket gateway): provider builders only reference files that live
146
+ * in the global upload store or inside the run's working directory — places
147
+ * the agent could already access on its own. Anything else (e.g. `~/.ssh`) is
148
+ * refused, so a caller-supplied descriptor can never leak arbitrary files.
149
+ */
150
+ export function isAllowedImageSourcePath(resolvedPath: string, cwd?: string): boolean {
151
+ return [getGlobalImageAssetsDir(), cwd || process.cwd()].some((directory) =>
152
+ getDirectoryPathVariants(directory).some((directoryVariant) =>
153
+ isPathInsideDirectory(resolvedPath, directoryVariant)
154
+ )
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Resolves the media type for one image, preferring the uploaded mime type and
160
+ * falling back to the file extension.
161
+ */
162
+ export function resolveImageMediaType(descriptor: ImageAttachmentDescriptor): string | null {
163
+ if (descriptor.mimeType) {
164
+ return descriptor.mimeType;
165
+ }
166
+ const extension = path.extname(descriptor.path).toLowerCase();
167
+ return EXTENSION_TO_MEDIA_TYPE[extension] || null;
168
+ }
169
+
170
+ const IMAGES_INPUT_TAG_PATTERN = /\s*<images_input>([\s\S]*?)<\/images_input>\s*/g;
171
+
172
+ // One image reference recovered from an <images_input> block: the stored
173
+ // asset path plus the user's original filename when it was recorded.
174
+ export type ParsedImageAttachment = {
175
+ path: string;
176
+ name?: string;
177
+ };
178
+
179
+ // Result of stripping an <images_input> block out of persisted prompt text.
180
+ // `imagePaths` mirrors `attachments` for callers that only need paths.
181
+ export type ParsedImagesInput = {
182
+ text: string;
183
+ imagePaths: string[];
184
+ attachments: ParsedImageAttachment[];
185
+ };
186
+
187
+ /**
188
+ * Appends the `<images_input>` reference block used by the Cursor and
189
+ * OpenCode CLIs. The block carries one numbered line per attachment with
190
+ * the stored file path (quote-free on purpose — Windows .cmd shims mangle
191
+ * quoted text) and the user's original filename, plus an explicit instruction
192
+ * to read the files and keep the block out of the reply. The same block is
193
+ * stripped back out of persisted history by {@link parseImagesInputTag}.
194
+ */
195
+ export function appendImagesInputTag(prompt: string, images: unknown): string {
196
+ const descriptors = normalizeImageDescriptors(images);
197
+ if (descriptors.length === 0) {
198
+ return prompt;
199
+ }
200
+
201
+ const entryLines = descriptors.map((descriptor, index) => {
202
+ const entryPath = toPosixPath(descriptor.path);
203
+ // Parentheses and newlines would break the "(original name: ...)" suffix
204
+ // the parser looks for, so drop them from the display name.
205
+ const cleanName = descriptor.name?.replace(/[()\r\n]/g, '').trim();
206
+ return cleanName
207
+ ? `${index + 1}. ${entryPath} (original name: ${cleanName})`
208
+ : `${index + 1}. ${entryPath}`;
209
+ });
210
+
211
+ return [
212
+ prompt,
213
+ '',
214
+ '<images_input>',
215
+ `The user attached ${descriptors.length} image(s) to this message. Read each file listed below with your file/image reading tool and use what you see to answer the prompt above. Respond as if the images were attached directly. Do not mention this block or the file paths unless the user asks about them.`,
216
+ ...entryLines,
217
+ '</images_input>',
218
+ ].join('\n');
219
+ }
220
+
221
+ const FILES_INPUT_TAG_PATTERN = /\s*<files_input>([\s\S]*?)<\/files_input>\s*/g;
222
+
223
+ /**
224
+ * Appends a provider-neutral file reference block to a prompt. Provider agents
225
+ * receive only server-validated paths from the global attachment store and can
226
+ * inspect each file with their normal filesystem tools.
227
+ */
228
+ export function appendFilesInputTag(prompt: string, files: unknown): string {
229
+ const descriptors = normalizeAttachmentDescriptors(files);
230
+ if (descriptors.length === 0) {
231
+ return prompt;
232
+ }
233
+
234
+ const entryLines = descriptors.map((descriptor, index) => {
235
+ const entryPath = toPosixPath(descriptor.path);
236
+ const cleanName = descriptor.name?.replace(/[()\r\n]/g, '').trim();
237
+ return cleanName
238
+ ? `${index + 1}. ${entryPath} (original name: ${cleanName})`
239
+ : `${index + 1}. ${entryPath}`;
240
+ });
241
+
242
+ return [
243
+ prompt,
244
+ '',
245
+ '<files_input>',
246
+ `The user attached ${descriptors.length} file(s) to this message. Read each file listed below with your file reading tools and use its contents to answer the prompt above. Do not mention this block or the file paths unless the user asks about them.`,
247
+ ...entryLines,
248
+ '</files_input>',
249
+ ].join('\n');
250
+ }
251
+
252
+ // Matches one numbered attachment entry inside the tag body. Works for both
253
+ // the multi-line block and the Windows-flattened single-line form, where the
254
+ // next ` N. ` marker (or the end of the body) delimits each entry.
255
+ const IMAGES_INPUT_ENTRY_PATTERN = /\d+\.\s+(.+?)(?=\s+\d+\.\s+|\s*$)/g;
256
+
257
+ const ORIGINAL_NAME_SUFFIX_PATTERN = /\(original name: ([^)]*)\)\s*$/;
258
+
259
+ function parseNumberedImageEntries(inner: string): ParsedImageAttachment[] {
260
+ const attachments: ParsedImageAttachment[] = [];
261
+ for (const entryMatch of inner.matchAll(IMAGES_INPUT_ENTRY_PATTERN)) {
262
+ let entryText = entryMatch[1].trim();
263
+ let name: string | undefined;
264
+
265
+ const nameMatch = ORIGINAL_NAME_SUFFIX_PATTERN.exec(entryText);
266
+ if (nameMatch) {
267
+ name = nameMatch[1].trim() || undefined;
268
+ entryText = entryText.slice(0, nameMatch.index).trim();
269
+ }
270
+
271
+ if (entryText) {
272
+ attachments.push(name ? { path: toPosixPath(entryText), name } : { path: toPosixPath(entryText) });
273
+ }
274
+ }
275
+ return attachments;
276
+ }
277
+
278
+ /**
279
+ * Strips the last provider-neutral file reference block from persisted prompt
280
+ * text and restores its attachment descriptors for chat history.
281
+ */
282
+ export function parseFilesInputTag(text: string): {
283
+ text: string;
284
+ filePaths: string[];
285
+ attachments: ParsedImageAttachment[];
286
+ } {
287
+ if (typeof text !== 'string' || !text.includes('<files_input>')) {
288
+ return { text, filePaths: [], attachments: [] };
289
+ }
290
+
291
+ let lastMatch: RegExpExecArray | null = null;
292
+ FILES_INPUT_TAG_PATTERN.lastIndex = 0;
293
+ for (let match = FILES_INPUT_TAG_PATTERN.exec(text); match; match = FILES_INPUT_TAG_PATTERN.exec(text)) {
294
+ lastMatch = match;
295
+ }
296
+ if (!lastMatch) {
297
+ return { text, filePaths: [], attachments: [] };
298
+ }
299
+
300
+ const attachments = parseNumberedImageEntries(lastMatch[1]);
301
+ const stripped = (
302
+ text.slice(0, lastMatch.index) + '\n' + text.slice(lastMatch.index + lastMatch[0].length)
303
+ ).trim();
304
+
305
+ return {
306
+ text: stripped,
307
+ filePaths: attachments.map((attachment) => attachment.path),
308
+ attachments,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Strips one `<images_input>` block from persisted prompt text and returns
314
+ * the clean text plus the referenced attachments (path and original name).
315
+ *
316
+ * Only the LAST block in the text is treated as the attachment carrier — the
317
+ * composer always appends it at the end, so a user who literally typed
318
+ * `<images_input>` earlier in their prompt keeps that text intact.
319
+ *
320
+ * Understands the numbered-line body in both its multi-line and
321
+ * Windows-flattened single-line forms.
322
+ */
323
+ export function parseImagesInputTag(text: string): ParsedImagesInput {
324
+ if (typeof text !== 'string' || !text.includes('<images_input>')) {
325
+ return { text, imagePaths: [], attachments: [] };
326
+ }
327
+
328
+ let lastMatch: RegExpExecArray | null = null;
329
+ IMAGES_INPUT_TAG_PATTERN.lastIndex = 0;
330
+ for (let match = IMAGES_INPUT_TAG_PATTERN.exec(text); match; match = IMAGES_INPUT_TAG_PATTERN.exec(text)) {
331
+ lastMatch = match;
332
+ }
333
+ if (!lastMatch) {
334
+ return { text, imagePaths: [], attachments: [] };
335
+ }
336
+
337
+ const attachments = parseNumberedImageEntries(lastMatch[1]);
338
+
339
+ const stripped = (
340
+ text.slice(0, lastMatch.index) + '\n' + text.slice(lastMatch.index + lastMatch[0].length)
341
+ ).trim();
342
+
343
+ return {
344
+ text: stripped,
345
+ imagePaths: attachments.map((attachment) => attachment.path),
346
+ attachments,
347
+ };
348
+ }
349
+
350
+ /** Maps raw image paths to the attachment shape carried by NormalizedMessage.images. */
351
+ export function toImageAttachments(imagePaths: string[]): Array<{ path: string }> {
352
+ return imagePaths.map((imagePath) => ({ path: toPosixPath(imagePath) }));
353
+ }
354
+
355
+ type ClaudeContentBlock =
356
+ | { type: 'text'; text: string }
357
+ | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
358
+
359
+ /**
360
+ * Builds the Claude user-message content list: the prompt text followed by one
361
+ * base64 `image` block per attachment. Images the Claude API cannot accept
362
+ * (e.g. SVG) or that fail to read are skipped with a warning so the prompt
363
+ * itself still goes through.
364
+ */
365
+ export async function buildClaudeUserContent(
366
+ prompt: string,
367
+ images: unknown,
368
+ cwd?: string,
369
+ ): Promise<ClaudeContentBlock[]> {
370
+ const blocks: ClaudeContentBlock[] = [{ type: 'text', text: prompt }];
371
+
372
+ for (const descriptor of normalizeImageDescriptors(images)) {
373
+ const mediaType = resolveImageMediaType(descriptor);
374
+ if (!mediaType || !CLAUDE_IMAGE_MEDIA_TYPES.has(mediaType)) {
375
+ console.warn(`[Images] Skipping unsupported Claude image type for ${descriptor.path}`);
376
+ continue;
377
+ }
378
+
379
+ const resolvedPath = resolveImageAbsolutePath(cwd, descriptor.path);
380
+ if (!isAllowedImageSourcePath(resolvedPath, cwd)) {
381
+ console.warn(`[Images] Refusing to read image outside allowed roots: ${descriptor.path}`);
382
+ continue;
383
+ }
384
+
385
+ try {
386
+ const canonicalPath = await fs.realpath(resolvedPath);
387
+ if (!isAllowedImageSourcePath(canonicalPath, cwd)) {
388
+ console.warn(`[Images] Refusing to read symlinked image outside allowed roots: ${descriptor.path}`);
389
+ continue;
390
+ }
391
+
392
+ const bytes = await fs.readFile(canonicalPath);
393
+ blocks.push({
394
+ type: 'image',
395
+ source: {
396
+ type: 'base64',
397
+ media_type: mediaType,
398
+ data: bytes.toString('base64'),
399
+ },
400
+ });
401
+ } catch (error) {
402
+ const message = error instanceof Error ? error.message : String(error);
403
+ console.warn(`[Images] Failed to read image ${descriptor.path}: ${message}`);
404
+ }
405
+ }
406
+
407
+ return blocks;
408
+ }
409
+
410
+ type CodexInputItem =
411
+ | { type: 'text'; text: string }
412
+ | { type: 'local_image'; path: string };
413
+
414
+ /**
415
+ * Builds the Codex `runStreamed` input list: prompt text plus one
416
+ * `local_image` item per attachment, resolved to absolute paths so the Codex
417
+ * runtime can read them regardless of its own working directory handling.
418
+ */
419
+ export function buildCodexInputItems(prompt: string, images: unknown, cwd?: string): CodexInputItem[] {
420
+ const items: CodexInputItem[] = [{ type: 'text', text: prompt }];
421
+ for (const descriptor of normalizeImageDescriptors(images)) {
422
+ const resolvedPath = resolveImageAbsolutePath(cwd, descriptor.path);
423
+ if (!isAllowedImageSourcePath(resolvedPath, cwd)) {
424
+ // Same trust boundary as buildClaudeUserContent — the Codex runtime
425
+ // reads this file, so it must stay within the allowed roots.
426
+ console.warn(`[Images] Refusing to attach image outside allowed roots: ${descriptor.path}`);
427
+ continue;
428
+ }
429
+ items.push({
430
+ type: 'local_image',
431
+ path: resolvedPath,
432
+ });
433
+ }
434
+ return items;
435
+ }