@cloudcli-ai/cloudcli 1.37.3 → 1.37.4

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 (295) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
  34. package/dist/assets/index-CdjIFxM2.js +5004 -0
  35. package/dist/assets/index-DgafhGTF.css +32 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  51. package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
  53. package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  55. package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  56. package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  58. package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/server/index.js +4 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/modules/agent/agent.module.js +9 -2
  63. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +175 -128
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  67. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  68. package/dist-server/server/modules/agent/tests/agent.routes.test.js +18 -1
  69. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  70. package/dist-server/server/modules/database/migrations.js +13 -0
  71. package/dist-server/server/modules/database/migrations.js.map +1 -1
  72. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  73. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  74. package/dist-server/server/modules/database/repositories/sessions.db.js +7 -0
  75. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  76. package/dist-server/server/modules/database/schema.js +7 -0
  77. package/dist-server/server/modules/database/schema.js.map +1 -1
  78. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  79. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  80. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  81. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  82. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  83. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  84. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  85. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  86. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  87. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  88. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  89. package/dist-server/server/modules/git/git.routes.js +92 -5
  90. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  91. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  92. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  93. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  94. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  95. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  96. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  97. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  98. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  99. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  100. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  101. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  102. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  103. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  104. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  105. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +42 -8
  106. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
  108. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  109. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +305 -16
  110. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  111. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  112. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  113. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +686 -77
  114. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -0
  116. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +11 -4
  118. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  119. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  122. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  123. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -2
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  125. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
  126. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  128. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  130. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/provider.routes.js +110 -2
  132. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  134. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  135. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  136. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +9 -1
  138. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  140. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  142. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  144. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  145. package/dist-server/server/modules/providers/services/session-history-cache.service.js +4 -0
  146. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
  147. package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
  148. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -0
  150. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  151. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  152. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  154. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  156. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  158. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1368 -12
  160. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  162. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  163. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  164. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  165. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +32 -0
  166. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
  167. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
  168. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  169. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  170. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/opencode-models.test.js +79 -0
  172. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  173. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  174. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  176. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  177. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  178. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  179. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  180. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  181. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +19 -0
  182. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
  183. package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
  184. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  185. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
  186. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
  187. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
  188. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
  189. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
  190. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  191. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
  192. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  193. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -20
  194. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  195. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  196. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  197. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  198. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  199. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +420 -0
  200. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  201. package/dist-server/server/shared/message-unification.js +1 -1
  202. package/dist-server/server/shared/message-unification.js.map +1 -1
  203. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  204. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  205. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  206. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  207. package/dist-server/server/shared/utils.js +126 -0
  208. package/dist-server/server/shared/utils.js.map +1 -1
  209. package/electron/desktopWindow.js +6 -0
  210. package/electron/viewHost.js +25 -2
  211. package/package.json +4 -4
  212. package/server/index.ts +5 -1
  213. package/server/modules/agent/agent.module.ts +11 -1
  214. package/server/modules/agent/agent.routes.ts +198 -140
  215. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  216. package/server/modules/agent/tests/agent.routes.test.ts +18 -1
  217. package/server/modules/database/migrations.ts +14 -0
  218. package/server/modules/database/repositories/provider-models.ts +79 -6
  219. package/server/modules/database/repositories/sessions.db.ts +8 -0
  220. package/server/modules/database/schema.ts +7 -0
  221. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  222. package/server/modules/file-tree/file-tree.module.ts +2 -1
  223. package/server/modules/file-tree/file-tree.service.ts +49 -7
  224. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  225. package/server/modules/git/git-branch-diff.service.ts +204 -0
  226. package/server/modules/git/git-branch.service.ts +2 -12
  227. package/server/modules/git/git.routes.ts +100 -6
  228. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  229. package/server/modules/projects/project-clone.routes.ts +142 -0
  230. package/server/modules/projects/projects.routes.ts +16 -81
  231. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  232. package/server/modules/projects/services/project-clone.service.ts +102 -34
  233. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  234. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  235. package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
  236. package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
  237. package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
  238. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  239. package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
  240. package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
  241. package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
  242. package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
  243. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  244. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
  245. package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
  246. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  247. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  248. package/server/modules/providers/provider.routes.ts +131 -2
  249. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  250. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  251. package/server/modules/providers/services/provider-capabilities.service.ts +17 -1
  252. package/server/modules/providers/services/provider-models.service.ts +71 -4
  253. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  254. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  255. package/server/modules/providers/services/session-history-cache.service.ts +13 -2
  256. package/server/modules/providers/services/sessions.service.ts +108 -8
  257. package/server/modules/providers/tests/claude-auth.test.ts +150 -0
  258. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  259. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  260. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  261. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  262. package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
  263. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  264. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  265. package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
  266. package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
  267. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  268. package/server/modules/providers/tests/opencode-models.test.ts +100 -0
  269. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  270. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  271. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  272. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  273. package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
  274. package/server/modules/providers/tests/sessions.service.test.ts +237 -1
  275. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
  276. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
  277. package/server/modules/websocket/README.md +4 -2
  278. package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
  279. package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
  280. package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
  281. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  282. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  283. package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
  284. package/server/shared/interfaces.ts +29 -0
  285. package/server/shared/message-unification.ts +1 -1
  286. package/server/shared/tests/read-only-roots.test.ts +139 -0
  287. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  288. package/server/shared/types.ts +261 -7
  289. package/server/shared/utils.ts +136 -0
  290. package/dist/assets/channel-C5xU9jMw.js +0 -1
  291. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
  292. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
  293. package/dist/assets/index-BTDHQ6Id.css +0 -32
  294. package/dist/assets/index-sFfz8iBR.js +0 -4931
  295. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
@@ -34,6 +34,37 @@ export const CODEX_PREDEFINED_MODELS: ProviderModelsDefinition = {
34
34
  ],
35
35
  },
36
36
  },
37
+ {
38
+ value: 'gpt-6-sol',
39
+ label: 'GPT-6 Sol',
40
+ description: 'Workhorse model for coding and everyday work.',
41
+ effort: {
42
+ default: 'medium',
43
+ values: [
44
+ { value: 'low' },
45
+ { value: 'medium' },
46
+ { value: 'high' },
47
+ { value: 'xhigh' },
48
+ { value: 'max' },
49
+ { value: 'ultra' },
50
+ ],
51
+ },
52
+ },
53
+ {
54
+ value: 'gpt-6-luna',
55
+ label: 'GPT-6 Luna',
56
+ description: 'Fast and affordable model for easier tasks.',
57
+ effort: {
58
+ default: 'medium',
59
+ values: [
60
+ { value: 'low' },
61
+ { value: 'medium' },
62
+ { value: 'high' },
63
+ { value: 'xhigh' },
64
+ { value: 'max' },
65
+ ],
66
+ },
67
+ },
37
68
  {
38
69
  value: 'gpt-5.6-sol',
39
70
  label: 'GPT-5.6 Sol',
@@ -34,6 +34,10 @@ type ActiveCodexSession = {
34
34
 
35
35
  const activeCodexSessions = new Map<string, ActiveCodexSession>();
36
36
 
37
+ // Codex CLI requires non-whitespace stdin even when --image arguments are
38
+ // present, so attachment-only turns need a small text instruction.
39
+ const CODEX_IMAGE_ONLY_PROMPT = 'Please analyze the attached image(s).';
40
+
37
41
  /**
38
42
  * Item types whose in-flight updates are worth showing. These are the ones a
39
43
  * user waits on — a shell command's output, an MCP call, and the running plan.
@@ -341,8 +345,12 @@ async function queryCodex(
341
345
  // Execute with streaming. Turns with image attachments send structured
342
346
  // input items so Codex reads the images from their local asset paths.
343
347
  const promptWithFiles = appendFilesInputTag(command, files);
344
- const turnInput = normalizeImageDescriptors(images).length > 0
345
- ? buildCodexInputItems(promptWithFiles, images, workingDirectory)
348
+ const normalizedImages = normalizeImageDescriptors(images);
349
+ const promptWithImageFallback = !promptWithFiles.trim() && normalizedImages.length > 0
350
+ ? CODEX_IMAGE_ONLY_PROMPT
351
+ : promptWithFiles;
352
+ const turnInput = normalizedImages.length > 0
353
+ ? buildCodexInputItems(promptWithImageFallback, normalizedImages, workingDirectory)
346
354
  : promptWithFiles;
347
355
  const streamedTurn = await thread.runStreamed(turnInput, {
348
356
  signal: abortController.signal
@@ -528,8 +536,8 @@ export const codexRuntime = {
528
536
  */
529
537
  function sendMessage(ws: ProviderRuntimeWriter, data: unknown) {
530
538
  try {
531
- if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
532
- // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
539
+ if (ws.isWebSocketWriter) {
540
+ // The gateway writer handles stringification
533
541
  ws.send(data);
534
542
  } else if (typeof ws.send === 'function') {
535
543
  // Raw WebSocket - stringify here
@@ -220,6 +220,39 @@ function extractCodexTextContent(content: unknown): string {
220
220
  .join('\n');
221
221
  }
222
222
 
223
+ /** Reads image inputs from a typed UserMessage item. */
224
+ function extractCodexTypedUserImages(
225
+ content: unknown,
226
+ ): Array<{ path?: string; data?: string }> | undefined {
227
+ if (!Array.isArray(content)) {
228
+ return undefined;
229
+ }
230
+
231
+ const attachments: Array<{ path?: string; data?: string }> = [];
232
+ for (const rawEntry of content) {
233
+ const entry = readObjectRecord(rawEntry);
234
+ if (!entry) {
235
+ continue;
236
+ }
237
+
238
+ const value = entry.type === 'local_image'
239
+ ? readNonEmptyString(entry.path)
240
+ : entry.type === 'image'
241
+ ? readNonEmptyString(entry.image_url)
242
+ : undefined;
243
+ if (!value) {
244
+ continue;
245
+ }
246
+ if (value.startsWith('data:')) {
247
+ attachments.push({ data: value });
248
+ } else {
249
+ attachments.push(...toImageAttachments([value]));
250
+ }
251
+ }
252
+
253
+ return attachments.length > 0 ? attachments : undefined;
254
+ }
255
+
223
256
  /**
224
257
  * Reads the markdown out of Codex's `<proposed_plan>` envelope.
225
258
  *
@@ -1312,6 +1345,36 @@ async function getCodexSessionMessages(sessionId: string): Promise<CodexHistoryR
1312
1345
  continue;
1313
1346
  }
1314
1347
 
1348
+ // Paginated rollouts carry user prompts as `item_completed` /
1349
+ // `UserMessage`; wire `response_item` user rows are internal, not prompts.
1350
+ if (payload.type === 'item_completed') {
1351
+ const item = readObjectRecord(payload.item);
1352
+ if (item?.type === 'UserMessage') {
1353
+ const content = extractCodexTextContent(item.content);
1354
+ const images = extractCodexTypedUserImages(item.content);
1355
+ if (content.trim() || images?.length) {
1356
+ // Only the first prompt of a turn is anchored, matching the legacy
1357
+ // `user_message` branch below: a turn can hold several prompts and
1358
+ // the edit cut is per turn.
1359
+ const turnId = readNonEmptyString(payload.turn_id)
1360
+ ?? turns.getCurrentTurnId();
1361
+ const isFirstPromptOfTurn = Boolean(turnId) && !anchoredTurnIds.has(turnId as string);
1362
+ if (isFirstPromptOfTurn) {
1363
+ anchoredTurnIds.add(turnId as string);
1364
+ }
1365
+ const row: AnyRecord = {
1366
+ type: 'user',
1367
+ timestamp,
1368
+ message: { role: 'user', content },
1369
+ ...(images ? { images } : {}),
1370
+ ...(isFirstPromptOfTurn ? { turnId } : {}),
1371
+ };
1372
+ messages.push(row);
1373
+ }
1374
+ }
1375
+ continue;
1376
+ }
1377
+
1315
1378
  if (isVisibleCodexUserMessage(payload)) {
1316
1379
  // Only the first prompt of a turn is anchored. A turn can hold more
1317
1380
  // than one — a follow-up queued while the turn was running is written
@@ -10,6 +10,12 @@ type CursorLoginStatus = {
10
10
  error?: string;
11
11
  };
12
12
 
13
+ // `cursor-agent status` decides "logged in" from the locally stored tokens, then fetches the
14
+ // account details from Cursor's API. That request cannot change the verdict (on failure the CLI
15
+ // still reports logged in), it only adds a network round trip to the CLI's startup (3-6 s on a
16
+ // busy host even when logged out). A 5 s cap reported fresh logins as "Command timeout" (#551).
17
+ const STATUS_TIMEOUT_MS = 20_000;
18
+
13
19
  export class CursorProviderAuth implements IProviderAuth {
14
20
  /**
15
21
  * Checks whether the cursor-agent CLI is available on this host.
@@ -71,7 +77,7 @@ export class CursorProviderAuth implements IProviderAuth {
71
77
  error: 'Command timeout',
72
78
  });
73
79
  }
74
- }, 5000);
80
+ }, STATUS_TIMEOUT_MS);
75
81
 
76
82
  try {
77
83
  childProcess = spawn('cursor-agent', ['status']);
@@ -112,8 +118,10 @@ export class CursorProviderAuth implements IProviderAuth {
112
118
  return;
113
119
  }
114
120
 
121
+ // e.g. "Logged in (unable to fetch user details)" when the account request failed.
122
+ // There is no email to show, so the UI uses its own fallback label.
115
123
  if (stdout.includes('Logged in')) {
116
- resolve({ authenticated: true, email: 'Logged in', method: 'cli' });
124
+ resolve({ authenticated: true, email: null, method: 'cli' });
117
125
  return;
118
126
  }
119
127
 
@@ -6,7 +6,7 @@ import {
6
6
  normalizeAttachmentDescriptors
7
7
  } from '@/shared/image-attachments.js';
8
8
  import { notifyRunFailed, notifyRunStopped } from '@/modules/notifications/index.js';
9
- import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell } from '@/shared/utils.js';
9
+ import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell, stripAnsiSequences } from '@/shared/utils.js';
10
10
 
11
11
  // cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
12
12
  // child_process.spawn everywhere else.
@@ -277,7 +277,14 @@ async function spawnCursor(command, options = {}, ws, context) {
277
277
  return;
278
278
  }
279
279
 
280
- ws.send(createNormalizedMessage({ kind: 'error', content: stderrText, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
280
+ // The CLI styles its stderr for a terminal; the chat renders plain
281
+ // text, so the escapes have to go before the text is surfaced.
282
+ const cleanedStderrText = stripAnsiSequences(stderrText);
283
+ if (!cleanedStderrText.trim()) {
284
+ return;
285
+ }
286
+
287
+ ws.send(createNormalizedMessage({ kind: 'error', content: cleanedStderrText, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
281
288
  });
282
289
 
283
290
  // Handle process completion
@@ -406,11 +406,26 @@ const parseOpenCodeSessionModelValue = (rawModel: unknown): string | null => {
406
406
  return null;
407
407
  }
408
408
 
409
- return readOptionalString(record.id)
409
+ const modelId = readOptionalString(record.id)
410
410
  ?? readOptionalString(record.model)
411
411
  ?? readOptionalString(record.name)
412
- ?? readOptionalString(record.value)
413
- ?? null;
412
+ ?? readOptionalString(record.value);
413
+ if (!modelId) {
414
+ return null;
415
+ }
416
+
417
+ // OpenCode persists the provider and model id in separate fields (for
418
+ // example {"id":"z-ai/glm-5.3-flash","providerID":"openrouter"}), and its
419
+ // CLI's `--model` flag only accepts the composed `<providerID>/<modelID>`
420
+ // form. Compose it here so a resumed session resolves the same model
421
+ // instead of failing with an unknown-model error.
422
+ const providerId = readOptionalString(record.providerID)
423
+ ?? readOptionalString(record.providerId);
424
+ if (providerId && !modelId.startsWith(`${providerId}/`)) {
425
+ return `${providerId}/${modelId}`;
426
+ }
427
+
428
+ return modelId;
414
429
  };
415
430
 
416
431
  /** Provider registry model adapter for OpenCode predefined models and session metadata. */
@@ -9,7 +9,7 @@ import {
9
9
  normalizeAttachmentDescriptors
10
10
  } from '@/shared/image-attachments.js';
11
11
  import { notifyRunFailed, notifyRunStopped } from '@/modules/notifications/index.js';
12
- import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell, getOpenCodeDatabasePath } from '@/shared/utils.js';
12
+ import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell, getOpenCodeDatabasePath, stripAnsiSequences } from '@/shared/utils.js';
13
13
 
14
14
  // cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
15
15
  // child_process.spawn everywhere else.
@@ -308,7 +308,10 @@ async function spawnOpenCode(command, options = {}, ws, context) {
308
308
  });
309
309
 
310
310
  opencodeProcess.stderr.on('data', (data) => {
311
- const stderrText = data.toString();
311
+ // opencode styles its stderr for a terminal; the chat renders plain
312
+ // text, so the escapes have to go before the text is surfaced. A chunk
313
+ // that was styling only cleans down to nothing and is not an error.
314
+ const stderrText = stripAnsiSequences(data.toString());
312
315
  if (!stderrText.trim()) {
313
316
  return;
314
317
  }
@@ -28,17 +28,23 @@ type SynchronizeRowsResult = {
28
28
  firstSessionId: string | null;
29
29
  };
30
30
 
31
+ type OpenCodeChildSessionRow = {
32
+ id: string;
33
+ };
34
+
31
35
  /**
32
36
  * Session indexer for OpenCode's SQLite-backed session store.
33
37
  */
34
38
  export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer {
35
39
  private readonly provider = 'opencode' as const;
40
+ private childSessionsReconciled = false;
36
41
 
37
42
  /**
38
43
  * Scans OpenCode's shared opencode.db and upserts active sessions into DB.
39
44
  */
40
45
  async synchronize(since?: Date): Promise<number> {
41
- const result = this.synchronizeRows(since);
46
+ // The first provider-wide scan also reconciles child rows indexed by older versions.
47
+ const result = this.synchronizeRows(since, undefined, !this.childSessionsReconciled);
42
48
  return result.processed;
43
49
  }
44
50
 
@@ -54,7 +60,11 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
54
60
  return result.firstSessionId;
55
61
  }
56
62
 
57
- private synchronizeRows(since?: Date, limit?: number): SynchronizeRowsResult {
63
+ private synchronizeRows(
64
+ since?: Date,
65
+ limit?: number,
66
+ pruneChildSessions = false,
67
+ ): SynchronizeRowsResult {
58
68
  const dbPath = getOpenCodeDatabasePath();
59
69
  if (!fsSync.existsSync(dbPath)) {
60
70
  return { processed: 0, firstSessionId: null };
@@ -62,6 +72,11 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
62
72
 
63
73
  const db = new Database(dbPath, { readonly: true, fileMustExist: true });
64
74
  try {
75
+ if (pruneChildSessions) {
76
+ this.pruneChildSessions(db);
77
+ this.childSessionsReconciled = true;
78
+ }
79
+
65
80
  const sinceMillis = since?.getTime() ?? null;
66
81
  const limitClause = limit ? 'LIMIT ?' : '';
67
82
  const params = limit ? [sinceMillis, sinceMillis, limit] : [sinceMillis, sinceMillis];
@@ -76,6 +91,7 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
76
91
  FROM session s
77
92
  LEFT JOIN project p ON p.id = s.project_id
78
93
  WHERE s.time_archived IS NULL
94
+ AND s.parent_id IS NULL
79
95
  AND (? IS NULL OR COALESCE(s.time_updated, s.time_created, 0) >= ?)
80
96
  ORDER BY COALESCE(s.time_updated, s.time_created, 0) DESC, s.id DESC
81
97
  ${limitClause}
@@ -105,6 +121,18 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
105
121
  }
106
122
  }
107
123
 
124
+ private pruneChildSessions(db: Database.Database): void {
125
+ const childSessions = db.prepare(`
126
+ SELECT id
127
+ FROM session
128
+ WHERE parent_id IS NOT NULL
129
+ `).all() as OpenCodeChildSessionRow[];
130
+
131
+ for (const childSession of childSessions) {
132
+ sessionsDb.deleteSessionByProviderSessionId(childSession.id, this.provider);
133
+ }
134
+ }
135
+
108
136
  private upsertSession(db: Database.Database, row: OpenCodeSessionRow): string | null {
109
137
  const sessionId = readOptionalString(row.id);
110
138
  const projectPath = readOptionalString(row.directory) ?? readOptionalString(row.worktree);
@@ -1,6 +1,7 @@
1
1
  import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
+ import { claudeSettingsPermissionsService } from '@/modules/providers/services/claude-settings-permissions.service.js';
4
5
  import { providerCapabilitiesService } from '@/modules/providers/services/provider-capabilities.service.js';
5
6
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
6
7
  import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
@@ -9,6 +10,7 @@ import { providerSkillsService } from '@/modules/providers/services/skills.servi
9
10
  import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
10
11
  import { sessionsService } from '@/modules/providers/services/sessions.service.js';
11
12
  import type {
13
+ CustomProviderModelEffort,
12
14
  CustomProviderModelInput,
13
15
  LLMProvider,
14
16
  McpScope,
@@ -53,6 +55,35 @@ const parseSessionId = (value: unknown): string => {
53
55
  return sessionId;
54
56
  };
55
57
 
58
+ /** A workflow run id as the harness mints one (`wf_16fbf852-274`). */
59
+ const WORKFLOW_RUN_ID_PATTERN = /^wf_[A-Za-z0-9-]+$/;
60
+ /** A workflow agent id as the CLI mints one: `a` and sixteen hex digits. */
61
+ const WORKFLOW_AGENT_ID_PATTERN = /^a[0-9a-f]{16}$/;
62
+
63
+ // Both ids name a file under the session's transcript directory, so nothing
64
+ // outside these shapes may reach the filesystem.
65
+ const parseWorkflowRunId = (value: unknown): string => {
66
+ const runId = readPathParam(value, 'runId');
67
+ if (!WORKFLOW_RUN_ID_PATTERN.test(runId)) {
68
+ throw new AppError('Invalid workflow run id.', {
69
+ code: 'INVALID_WORKFLOW_RUN_ID',
70
+ statusCode: 400,
71
+ });
72
+ }
73
+ return runId;
74
+ };
75
+
76
+ const parseWorkflowAgentId = (value: unknown): string => {
77
+ const agentId = readPathParam(value, 'agentId');
78
+ if (!WORKFLOW_AGENT_ID_PATTERN.test(agentId)) {
79
+ throw new AppError('Invalid workflow agent id.', {
80
+ code: 'INVALID_WORKFLOW_AGENT_ID',
81
+ statusCode: 400,
82
+ });
83
+ }
84
+ return agentId;
85
+ };
86
+
56
87
  const readOptionalQueryString = (value: unknown): string | undefined => {
57
88
  if (typeof value !== 'string') {
58
89
  return undefined;
@@ -465,6 +496,78 @@ const parseModelRecordId = (value: unknown): number => {
465
496
  return recordId;
466
497
  };
467
498
 
499
+ const invalidModelEffort = (message: string): AppError => new AppError(message, {
500
+ code: 'INVALID_MODEL_EFFORT',
501
+ statusCode: 400,
502
+ });
503
+
504
+ /**
505
+ * Upper bound on declared effort levels, far above any provider's level set.
506
+ * Checked before the per-entry loop so an oversized body (the JSON parser
507
+ * accepts up to 50 MB) is rejected at once instead of being walked in full.
508
+ */
509
+ const MAX_MODEL_EFFORT_LEVELS = 32;
510
+
511
+ /**
512
+ * Parses the optional `effort` field of a custom-model payload.
513
+ *
514
+ * Only the shape is checked here (unique, trimmed, non-empty level ids and a
515
+ * default drawn from them); which levels a provider accepts is decided by the
516
+ * Providers service. `undefined` means "not sent" and `null` or an empty
517
+ * `values` list means "no effort levels".
518
+ */
519
+ const parseCustomProviderModelEffort = (
520
+ value: unknown,
521
+ ): CustomProviderModelEffort | null | undefined => {
522
+ if (value === undefined) {
523
+ return undefined;
524
+ }
525
+ if (value === null) {
526
+ return null;
527
+ }
528
+ if (typeof value !== 'object' || Array.isArray(value)) {
529
+ throw invalidModelEffort('effort must be an object with a values array.');
530
+ }
531
+
532
+ const effort = value as Record<string, unknown>;
533
+ if (!Array.isArray(effort.values)) {
534
+ throw invalidModelEffort('effort.values must be an array of effort levels.');
535
+ }
536
+ if (effort.values.length > MAX_MODEL_EFFORT_LEVELS) {
537
+ throw invalidModelEffort(`effort.values can list at most ${MAX_MODEL_EFFORT_LEVELS} effort levels.`);
538
+ }
539
+
540
+ const values: string[] = [];
541
+ const seenLevels = new Set<string>();
542
+ for (const entry of effort.values) {
543
+ const level = typeof entry === 'string' ? entry.trim() : '';
544
+ if (!level || level.length > 40) {
545
+ throw invalidModelEffort('effort.values must contain non-empty strings of 40 characters or fewer.');
546
+ }
547
+ if (seenLevels.has(level)) {
548
+ throw invalidModelEffort(`effort.values contains "${level}" more than once.`);
549
+ }
550
+ seenLevels.add(level);
551
+ values.push(level);
552
+ }
553
+
554
+ if (effort.default !== undefined && effort.default !== null && typeof effort.default !== 'string') {
555
+ throw invalidModelEffort('effort.default must be a string.');
556
+ }
557
+ const defaultLevel = readOptionalQueryString(effort.default);
558
+ if (values.length === 0) {
559
+ if (defaultLevel) {
560
+ throw invalidModelEffort('effort.default requires at least one effort level.');
561
+ }
562
+ return null;
563
+ }
564
+ if (defaultLevel && !values.includes(defaultLevel)) {
565
+ throw invalidModelEffort('effort.default must be one of effort.values.');
566
+ }
567
+
568
+ return defaultLevel ? { values, default: defaultLevel } : { values };
569
+ };
570
+
468
571
  const parseCustomProviderModelPayload = (payload: unknown): CustomProviderModelInput => {
469
572
  if (!payload || typeof payload !== 'object') {
470
573
  throw new AppError('Request body must be an object.', {
@@ -501,7 +604,8 @@ const parseCustomProviderModelPayload = (payload: unknown): CustomProviderModelI
501
604
  });
502
605
  }
503
606
 
504
- return { model, id };
607
+ const effort = parseCustomProviderModelEffort(body.effort);
608
+ return effort === undefined ? { model, id } : { model, id, effort };
505
609
  };
506
610
 
507
611
  router.get(
@@ -717,6 +821,15 @@ router.get(
717
821
  }),
718
822
  );
719
823
 
824
+ // Read-only: the rules Claude's own settings files add on top of the UI's lists.
825
+ router.get(
826
+ '/claude/settings-permissions',
827
+ asyncHandler(async (_req: Request, res: Response) => {
828
+ const sources = await claudeSettingsPermissionsService.listRuleSources();
829
+ res.json(createApiSuccessResponse({ sources }));
830
+ }),
831
+ );
832
+
720
833
  // ----------------- Session routes -----------------
721
834
  /**
722
835
  * Session gateway entry point: allocates the stable app-facing session id for
@@ -739,7 +852,7 @@ router.post(
739
852
  router.get(
740
853
  '/sessions/running',
741
854
  asyncHandler(async (_req: Request, res: Response) => {
742
- const sessions = sessionsService.listRunningSessions();
855
+ const sessions = await sessionsService.listRunningSessions();
743
856
  res.json(createApiSuccessResponse({ sessions }));
744
857
  }),
745
858
  );
@@ -848,6 +961,22 @@ router.get(
848
961
  }),
849
962
  );
850
963
 
964
+ /**
965
+ * One workflow agent's timeline, read on demand when its row in the workflow
966
+ * card is opened. History does not carry it: a run can spawn a dozen agents
967
+ * with hundreds of tool calls each, and the card only lists them.
968
+ */
969
+ router.get(
970
+ '/sessions/:sessionId/workflows/:runId/agents/:agentId',
971
+ asyncHandler(async (req: Request, res: Response) => {
972
+ const sessionId = parseSessionId(req.params.sessionId);
973
+ const runId = parseWorkflowRunId(req.params.runId);
974
+ const agentId = parseWorkflowAgentId(req.params.agentId);
975
+ const result = await sessionsService.readWorkflowAgentActivity(sessionId, runId, agentId);
976
+ res.json(createApiSuccessResponse(result));
977
+ }),
978
+ );
979
+
851
980
  router.get('/search/sessions', asyncHandler(async (req: Request, res: Response) => {
852
981
  const query = parseSessionSearchQuery(req.query.q);
853
982
  const limit = parseSessionSearchLimit(req.query.limit);
@@ -0,0 +1,121 @@
1
+ import fsp from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * One Claude CLI process that is currently producing a response.
7
+ *
8
+ * `providerSessionId` is Claude's own session id, which is what the registry
9
+ * records; callers map it onto an app session themselves.
10
+ */
11
+ type LiveClaudeCliSession = {
12
+ providerSessionId: string;
13
+ startedAt: number;
14
+ };
15
+
16
+ /**
17
+ * Claude Code keeps one file per running session here, named `<pid>.json`.
18
+ * The directory belongs to the CLI, not to CloudCLI: it is only ever read.
19
+ */
20
+ const registryDirectory = () => path.join(os.homedir(), '.claude', 'sessions');
21
+
22
+ /**
23
+ * Upper bound on registry files inspected per poll.
24
+ *
25
+ * The running-sessions endpoint is polled continuously, so a directory left
26
+ * full of stale files by repeated crashes must not turn every poll into
27
+ * unbounded filesystem work. Real installations hold a handful of entries.
28
+ */
29
+ const MAX_REGISTRY_FILES = 256;
30
+
31
+ /**
32
+ * Whether `pid` is still the process the registry recorded.
33
+ *
34
+ * A registry file outlives a crash, so its mere presence proves nothing. The
35
+ * signal-0 probe answers "does this pid exist" (EPERM means it exists but is
36
+ * owned by someone else, which still counts), and on Linux the recorded
37
+ * `procStart` is compared against the kernel's start-time for that pid so a
38
+ * reused pid cannot be mistaken for the original process.
39
+ */
40
+ async function isRecordedProcessAlive(pid: number, procStart: unknown): Promise<boolean> {
41
+ try {
42
+ process.kill(pid, 0);
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException)?.code !== 'EPERM') {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ if (process.platform !== 'linux' || typeof procStart !== 'string' || !procStart) {
50
+ return true;
51
+ }
52
+
53
+ try {
54
+ const stat = await fsp.readFile(`/proc/${pid}/stat`, 'utf8');
55
+ // The comm field is parenthesised and may itself contain spaces, so the
56
+ // fields are counted from the last ')'. starttime is the 22nd field
57
+ // overall, i.e. the 20th of what follows the comm field.
58
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
59
+ return fields[19] === procStart;
60
+ } catch {
61
+ // No procfs entry means the process is gone between the two checks.
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /** Reads one registry file, returning null for anything that is not a live busy session. */
67
+ async function readLiveBusySession(filePath: string): Promise<LiveClaudeCliSession | null> {
68
+ let parsed: unknown;
69
+ try {
70
+ parsed = JSON.parse(await fsp.readFile(filePath, 'utf8'));
71
+ } catch {
72
+ // Unreadable, half-written or malformed entries are simply not evidence
73
+ // of a running session.
74
+ return null;
75
+ }
76
+
77
+ const record = parsed as Record<string, unknown> | null;
78
+ if (!record || typeof record !== 'object') {
79
+ return null;
80
+ }
81
+
82
+ const providerSessionId = typeof record.sessionId === 'string' ? record.sessionId : '';
83
+ const pid = typeof record.pid === 'number' ? record.pid : NaN;
84
+ if (!providerSessionId || !Number.isInteger(pid) || pid <= 0 || record.status !== 'busy') {
85
+ return null;
86
+ }
87
+
88
+ if (!(await isRecordedProcessAlive(pid, record.procStart))) {
89
+ return null;
90
+ }
91
+
92
+ const startedAt = typeof record.startedAt === 'number' ? record.startedAt : Date.now();
93
+ return { providerSessionId, startedAt };
94
+ }
95
+
96
+ /**
97
+ * Lists the Claude CLI sessions that are mid-turn right now.
98
+ *
99
+ * Used by the providers module's sessions service so the running-sessions poll
100
+ * also reports turns driven outside CloudCLI's own chat runs — including the
101
+ * `claude` processes CloudCLI itself spawns for the Shell view, which live in
102
+ * the PTY map rather than the chat-run registry. Liveness is ephemeral, so it
103
+ * is recomputed on every read instead of being cached or persisted.
104
+ */
105
+ export async function listBusyClaudeCliSessions(): Promise<LiveClaudeCliSession[]> {
106
+ let entries: string[];
107
+ try {
108
+ entries = await fsp.readdir(registryDirectory());
109
+ } catch {
110
+ // No registry directory at all is the normal case for an install that has
111
+ // never run the CLI, and an unreadable one is not worth failing a poll for.
112
+ return [];
113
+ }
114
+
115
+ const registryFiles = entries.filter((entry) => entry.endsWith('.json')).slice(0, MAX_REGISTRY_FILES);
116
+ const sessions = await Promise.all(
117
+ registryFiles.map((entry) => readLiveBusySession(path.join(registryDirectory(), entry))),
118
+ );
119
+
120
+ return sessions.filter((session): session is LiveClaudeCliSession => session !== null);
121
+ }