@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
@@ -1,8 +1,15 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { execFile } from 'node:child_process';
2
3
  import { EventEmitter } from 'node:events';
4
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
5
+ import http from 'node:http';
6
+ import type { AddressInfo } from 'node:net';
7
+ import os from 'node:os';
3
8
  import path from 'node:path';
4
9
  import { PassThrough } from 'node:stream';
5
10
  import test from 'node:test';
11
+ import { pathToFileURL } from 'node:url';
12
+ import { promisify } from 'node:util';
6
13
 
7
14
  import { startCloneProject } from '@/modules/projects/services/project-clone.service.js';
8
15
  import { AppError } from '@/shared/utils.js';
@@ -110,6 +117,66 @@ test('startCloneProject rejects github URL values that begin with option prefixe
110
117
  );
111
118
  });
112
119
 
120
+ for (const [label, githubUrl] of [
121
+ ['a token as the user', 'https://ghp_supersecrettoken1234567890abcd@github.com/example/repo.git'],
122
+ ['a user and password', 'https://user:ghp_supersecrettoken1234567890abcd@github.com/example/repo.git'],
123
+ ['a password on an ssh URL', 'ssh://git:secret@github.com/example/repo.git'],
124
+ ] as const) {
125
+ test(`startCloneProject rejects a github URL carrying ${label} before git sees it`, async () => {
126
+ // A credential in the URL would ride git's argv, its stderr and the
127
+ // clone's `.git/config` — every channel the token field keeps it out of.
128
+ let spawned = 0;
129
+ await assert.rejects(
130
+ async () =>
131
+ startCloneProject(
132
+ { workspacePath: '/workspace/root', githubUrl, userId: 1 },
133
+ { onProgress: () => undefined, onComplete: () => undefined },
134
+ buildDependencies({ spawnGitClone: () => { spawned += 1; throw new Error('must not spawn'); } }),
135
+ ),
136
+ (error: unknown) => {
137
+ assert.ok(error instanceof AppError);
138
+ assert.equal(error.code, 'GITHUB_URL_CARRIES_CREDENTIALS');
139
+ assert.ok(!error.message.includes('ghp_supersecrettoken'), 'the error must not echo the credential');
140
+ return true;
141
+ },
142
+ );
143
+ assert.equal(spawned, 0);
144
+ });
145
+ }
146
+
147
+ test('startCloneProject rejects an http(s) URL the parser cannot read before git carries it', async () => {
148
+ // 'https://token@github.com:notaport/x' is no URL to the parser, so the
149
+ // credential check could not see the token — and git would have had it on
150
+ // its command line until curl refused the port.
151
+ let spawned = 0;
152
+ await assert.rejects(
153
+ async () =>
154
+ startCloneProject(
155
+ { workspacePath: '/workspace/root', githubUrl: 'https://ghp_supersecrettoken1234567890abcd@github.com:notaport/example/repo.git', userId: 1 },
156
+ { onProgress: () => undefined, onComplete: () => undefined },
157
+ buildDependencies({ spawnGitClone: () => { spawned += 1; throw new Error('must not spawn'); } }),
158
+ ),
159
+ (error: unknown) => {
160
+ assert.ok(error instanceof AppError);
161
+ assert.equal(error.code, 'INVALID_GITHUB_URL');
162
+ return true;
163
+ },
164
+ );
165
+ assert.equal(spawned, 0);
166
+ });
167
+
168
+ test('startCloneProject accepts the ssh login name that is not a credential', async () => {
169
+ // `git@` on an SSH URL is the login every SSH clone uses, not a secret.
170
+ const gitProcess = createMockGitProcess();
171
+ const operation = await startCloneProject(
172
+ { workspacePath: '/workspace/root', githubUrl: 'ssh://git@github.com/example/repo.git', userId: 1 },
173
+ { onProgress: () => undefined, onComplete: () => undefined },
174
+ buildDependencies({ spawnGitClone: () => gitProcess as never }),
175
+ );
176
+ gitProcess.emit('close', 0);
177
+ await operation.waitForCompletion;
178
+ });
179
+
113
180
  test('startCloneProject rejects when selected github token does not exist', async () => {
114
181
  await assert.rejects(
115
182
  async () =>
@@ -181,3 +248,312 @@ test('startCloneProject completes and emits complete payload when git exits succ
181
248
  assert.equal(resolvedCompletePayload.message, 'Repository cloned successfully');
182
249
  assert.equal((resolvedCompletePayload.project.projectId as string) || '', 'project-1');
183
250
  });
251
+
252
+ type SpawnCall = { cloneUrl: string; clonePath: string; environment: NodeJS.ProcessEnv };
253
+
254
+ /**
255
+ * Runs a clone against a mock git process and captures exactly what would be
256
+ * handed to `spawn`: the URL and path (the only variable argv elements) and
257
+ * the environment. The token has to reach git through the environment alone.
258
+ */
259
+ async function captureSpawnCall(input: {
260
+ githubTokenId?: number;
261
+ newGithubToken?: string;
262
+ storedToken?: string;
263
+ }): Promise<SpawnCall> {
264
+ const gitProcess = createMockGitProcess();
265
+ let spawnCall: SpawnCall | null = null;
266
+
267
+ const operation = await startCloneProject(
268
+ {
269
+ workspacePath: '/workspace/root',
270
+ githubUrl: 'https://github.com/example/repo.git',
271
+ githubTokenId: input.githubTokenId,
272
+ newGithubToken: input.newGithubToken,
273
+ userId: 1,
274
+ },
275
+ {
276
+ onProgress: () => undefined,
277
+ onComplete: () => undefined,
278
+ },
279
+ buildDependencies({
280
+ getGithubTokenById: async () => ({ github_token: input.storedToken ?? 'unused' }),
281
+ spawnGitClone: (cloneUrl, clonePath, environment) => {
282
+ spawnCall = { cloneUrl, clonePath, environment };
283
+ return gitProcess as any;
284
+ },
285
+ }),
286
+ );
287
+
288
+ gitProcess.emit('close', 0);
289
+ await operation.waitForCompletion;
290
+
291
+ assert.notEqual(spawnCall, null, 'git was never spawned');
292
+ return spawnCall as unknown as SpawnCall;
293
+ }
294
+
295
+ for (const [label, input, token] of [
296
+ ['a stored token', { githubTokenId: 7, storedToken: 'ghp_storedsecret1234567890' }, 'ghp_storedsecret1234567890'],
297
+ // Characters `url.username=` would have percent-encoded, so a check for the
298
+ // raw token alone could not have caught the URL channel.
299
+ ['a new token', { newGithubToken: 'ghp_new:secret@with/odd-chars' }, 'ghp_new:secret@with/odd-chars'],
300
+ ] as const) {
301
+ test(`startCloneProject hands git ${label} through the credential helper, never the clone URL`, async () => {
302
+ const { cloneUrl, clonePath, environment } = await captureSpawnCall(input);
303
+
304
+ // The URL git receives is the one the user typed: no username, no password,
305
+ // nothing encoded — so neither argv, stderr nor `.git/config` can carry it.
306
+ assert.equal(cloneUrl, 'https://github.com/example/repo.git');
307
+ assert.equal(clonePath, path.join('/workspace/root', 'repo'));
308
+
309
+ assert.equal(environment.CLOUDCLI_GITHUB_TOKEN, token);
310
+ assert.equal(environment.GIT_CONFIG_COUNT, '2');
311
+ assert.equal(environment.GIT_CONFIG_KEY_0, 'credential.helper');
312
+ assert.equal(environment.GIT_CONFIG_VALUE_0, '');
313
+ assert.equal(environment.GIT_CONFIG_KEY_1, 'credential.https://github.com.helper');
314
+ assert.match(environment.GIT_CONFIG_VALUE_1 ?? '', /\$CLOUDCLI_GITHUB_TOKEN/);
315
+ assert.equal(environment.GIT_TERMINAL_PROMPT, '0');
316
+
317
+ // The helper's command line is itself a process argv, so the token must be
318
+ // read from the variable rather than pasted into the helper text.
319
+ for (const [name, value] of Object.entries(environment)) {
320
+ if (name === 'CLOUDCLI_GITHUB_TOKEN') {
321
+ continue;
322
+ }
323
+ assert.ok(!(value ?? '').includes(token), `token leaked into ${name}`);
324
+ }
325
+ });
326
+ }
327
+
328
+ test('startCloneProject leaves the credential helper unset when no token was given', async () => {
329
+ const { cloneUrl, environment } = await captureSpawnCall({});
330
+
331
+ assert.equal(cloneUrl, 'https://github.com/example/repo.git');
332
+ assert.equal(environment.GIT_TERMINAL_PROMPT, '0');
333
+ assert.equal(environment.GIT_CONFIG_COUNT, undefined);
334
+ assert.equal(environment.CLOUDCLI_GITHUB_TOKEN, undefined);
335
+ });
336
+
337
+ /**
338
+ * `git clone --progress` writes every byte of its progress to stderr — stdout
339
+ * stays empty — so these tests drive stderr, the pipe that carries the progress
340
+ * the user watches and the reason a clone failed.
341
+ */
342
+ async function runCloneWithStderr(
343
+ chunks: string[],
344
+ exitCode: number | null,
345
+ ): Promise<{ progressMessages: string[]; failure: AppError | null }> {
346
+ const gitProcess = createMockGitProcess();
347
+ const progressMessages: string[] = [];
348
+
349
+ const operation = await startCloneProject(
350
+ {
351
+ workspacePath: '/workspace/root',
352
+ githubUrl: 'https://github.com/example/repo.git',
353
+ newGithubToken: 'ghp_supersecrettoken1234567890abcd',
354
+ userId: 1,
355
+ },
356
+ {
357
+ onProgress: (message) => {
358
+ progressMessages.push(message);
359
+ },
360
+ onComplete: () => undefined,
361
+ },
362
+ buildDependencies({ spawnGitClone: () => gitProcess as any }),
363
+ );
364
+
365
+ for (const chunk of chunks) {
366
+ gitProcess.stderr.write(chunk);
367
+ }
368
+ gitProcess.stderr.end();
369
+ // Let the PassThrough deliver every `data` event and its `end` before the
370
+ // process is reported closed, exactly as a real pipe does.
371
+ await new Promise((resolve) => setImmediate(resolve));
372
+ gitProcess.emit('close', exitCode);
373
+
374
+ let failure: AppError | null = null;
375
+ try {
376
+ await operation.waitForCompletion;
377
+ } catch (error) {
378
+ failure = error as AppError;
379
+ }
380
+
381
+ return { progressMessages, failure };
382
+ }
383
+
384
+ test('startCloneProject forwards each stderr chunk to onProgress as it arrives', async () => {
385
+ const gitProcess = createMockGitProcess();
386
+ const progressMessages: string[] = [];
387
+
388
+ const operation = await startCloneProject(
389
+ {
390
+ workspacePath: '/workspace/root',
391
+ githubUrl: 'https://github.com/example/repo.git',
392
+ newGithubToken: 'ghp_supersecrettoken1234567890abcd',
393
+ userId: 1,
394
+ },
395
+ {
396
+ onProgress: (message) => {
397
+ progressMessages.push(message);
398
+ },
399
+ onComplete: () => undefined,
400
+ },
401
+ buildDependencies({ spawnGitClone: () => gitProcess as any }),
402
+ );
403
+
404
+ assert.deepEqual(progressMessages, ["Cloning into 'repo'..."]);
405
+
406
+ // Each chunk has to surface on its own event before the next one is written
407
+ // and long before the process exits: the progress UI is fed by these events,
408
+ // so anything buffered until close would leave the user staring at nothing.
409
+ const chunks = [
410
+ 'remote: Enumerating objects: 20, done.\n',
411
+ 'Receiving objects: 45% (9/20)\r',
412
+ 'Receiving objects: 100% (20/20), done.\n',
413
+ ];
414
+ for (const [index, chunk] of chunks.entries()) {
415
+ gitProcess.stderr.write(chunk);
416
+ await new Promise((resolve) => setImmediate(resolve));
417
+ assert.deepEqual(
418
+ progressMessages,
419
+ ["Cloning into 'repo'...", ...chunks.slice(0, index + 1).map((line) => line.trim())],
420
+ `chunk ${index + 1} did not arrive on its own event`,
421
+ );
422
+ }
423
+
424
+ gitProcess.stderr.end();
425
+ gitProcess.emit('close', 0);
426
+ await operation.waitForCompletion;
427
+
428
+ assert.deepEqual(progressMessages, [
429
+ "Cloning into 'repo'...",
430
+ 'remote: Enumerating objects: 20, done.',
431
+ 'Receiving objects: 45% (9/20)',
432
+ 'Receiving objects: 100% (20/20), done.',
433
+ ]);
434
+ });
435
+
436
+ test('startCloneProject reports the git failure reason from stderr', async () => {
437
+ const { failure } = await runCloneWithStderr(
438
+ ["remote: Repository not found.\nfatal: repository 'https://github.com/example/repo.git/' not found\n"],
439
+ 128,
440
+ );
441
+
442
+ assert.equal(failure?.code, 'GIT_CLONE_FAILED');
443
+ assert.equal(failure?.message, 'Repository not found. Please check the URL and ensure you have access.');
444
+ });
445
+
446
+ test('startCloneProject falls back to the last stderr text for an unrecognised failure', async () => {
447
+ const { failure } = await runCloneWithStderr(
448
+ ["fatal: unable to access 'https://github.com/example/repo.git/': SSL error\n"],
449
+ 128,
450
+ );
451
+
452
+ assert.equal(failure?.code, 'GIT_CLONE_FAILED');
453
+ assert.equal(failure?.message, "fatal: unable to access 'https://github.com/example/repo.git/': SSL error");
454
+ });
455
+
456
+ const execFileAsync = promisify(execFile);
457
+
458
+ /**
459
+ * Real git, real clone: proves the credential-helper environment is something
460
+ * git accepts, and that the resulting repository records a credential-free
461
+ * remote. The `file://` transport never asks the helper for anything, so a
462
+ * placeholder token stands in for the real one.
463
+ */
464
+ test('startCloneProject clones a real repository with the credential helper in git environment', async () => {
465
+ const root = await mkdtemp(path.join(os.tmpdir(), 'project-clone-service-'));
466
+ try {
467
+ const seedPath = path.join(root, 'seed');
468
+ const originPath = path.join(root, 'origin.git');
469
+ const workspacePath = path.join(root, 'workspace');
470
+ await execFileAsync('git', ['init', '-q', seedPath]);
471
+ await execFileAsync('git', [
472
+ '-C', seedPath,
473
+ '-c', 'user.email=clone@example.test',
474
+ '-c', 'user.name=Clone Test',
475
+ 'commit', '-q', '--allow-empty', '-m', 'initial',
476
+ ]);
477
+ await execFileAsync('git', ['clone', '-q', '--bare', seedPath, originPath]);
478
+
479
+ const originUrl = pathToFileURL(originPath).href;
480
+ const token = 'ghp_placeholder-file-transport-never-asks';
481
+ const progressMessages: string[] = [];
482
+ let completeMessage = '';
483
+
484
+ const operation = await startCloneProject(
485
+ {
486
+ workspacePath,
487
+ githubUrl: originUrl,
488
+ newGithubToken: token,
489
+ userId: 1,
490
+ },
491
+ {
492
+ onProgress: (message) => {
493
+ progressMessages.push(message);
494
+ },
495
+ onComplete: ({ message }) => {
496
+ completeMessage = message;
497
+ },
498
+ },
499
+ {
500
+ validatePath: async () => ({ valid: true, resolvedPath: workspacePath }),
501
+ registerProject: async (projectPath, customName) => ({
502
+ project: { path: projectPath, name: customName },
503
+ }),
504
+ },
505
+ );
506
+ await operation.waitForCompletion;
507
+
508
+ assert.equal(completeMessage, 'Repository cloned successfully');
509
+ // The first message is synthesised by the service; everything after it
510
+ // came off git's own stderr, which is what the progress UI relies on.
511
+ assert.ok(
512
+ progressMessages.length >= 2,
513
+ `no progress arrived from git: ${JSON.stringify(progressMessages)}`,
514
+ );
515
+
516
+ const cloneConfig = await readFile(path.join(workspacePath, 'origin', '.git', 'config'), 'utf8');
517
+ assert.match(cloneConfig, new RegExp(`url = ${originUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
518
+ assert.ok(!cloneConfig.includes(token), `token written to .git/config: ${cloneConfig}`);
519
+ } finally {
520
+ await rm(root, { recursive: true, force: true });
521
+ }
522
+ });
523
+
524
+ test('startCloneProject offers the GitHub token to no host but github.com', async () => {
525
+ // The helper used to answer every credential challenge: a clone from any
526
+ // host the user typed — over plain http too — received the GitHub token.
527
+ // Real git against a local server that challenges for Basic auth.
528
+ const root = await mkdtemp(path.join(os.tmpdir(), 'project-clone-scope-'));
529
+ const authorizations: Array<string | null> = [];
530
+ const server = http.createServer((request, response) => {
531
+ authorizations.push(request.headers.authorization ?? null);
532
+ response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="probe"' });
533
+ response.end('auth required');
534
+ });
535
+ await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve); });
536
+ const { port } = server.address() as AddressInfo;
537
+ const token = 'ghp_supersecrettoken1234567890abcd';
538
+
539
+ try {
540
+ let failure = '';
541
+ const operation = await startCloneProject(
542
+ { workspacePath: root, githubUrl: `http://127.0.0.1:${port}/example/repo.git`, newGithubToken: token, userId: 1 },
543
+ { onProgress: () => undefined, onComplete: () => undefined },
544
+ { validatePath: async () => ({ valid: true, resolvedPath: root }) },
545
+ );
546
+ await operation.waitForCompletion.catch((error: unknown) => { failure = error instanceof Error ? error.message : String(error); });
547
+
548
+ assert.ok(authorizations.length >= 1, 'git never reached the server');
549
+ assert.ok(
550
+ authorizations.every((authorization) => authorization === null),
551
+ `the token was offered to a foreign host: ${JSON.stringify(authorizations)}`,
552
+ );
553
+ assert.match(failure, /Authentication failed/);
554
+ } finally {
555
+ server.close();
556
+ await rm(root, { recursive: true, force: true });
557
+ }
558
+ });
559
+
@@ -6,7 +6,7 @@ import spawn from 'cross-spawn';
6
6
 
7
7
  import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
8
8
  import type { IProviderAuth } from '@/shared/interfaces.js';
9
- import type { ProviderAuthStatus } from '@/shared/types.js';
9
+ import type { ProviderAuthStatus, ProviderAuthSubscriptionOverride } from '@/shared/types.js';
10
10
  import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
11
11
 
12
12
  type ClaudeCredentialsStatus = {
@@ -14,6 +14,16 @@ type ClaudeCredentialsStatus = {
14
14
  email: string | null;
15
15
  method: string | null;
16
16
  error?: string;
17
+ subscriptionOverride?: ProviderAuthSubscriptionOverride;
18
+ };
19
+
20
+ /**
21
+ * An API-key style credential found in the env or settings.json, plus the
22
+ * user-facing label the settings UI has always shown for it. The labels are
23
+ * kept verbatim so existing consumers of `email` see no change.
24
+ */
25
+ type ClaudeApiKeyCredential = Pick<ProviderAuthSubscriptionOverride, 'variable' | 'source'> & {
26
+ label: string;
17
27
  };
18
28
 
19
29
  const hasErrorCode = (error: unknown, code: string): boolean => (
@@ -62,6 +72,7 @@ export class ClaudeProviderAuth implements IProviderAuth {
62
72
  email: credentials.authenticated ? credentials.email || 'Authenticated' : credentials.email,
63
73
  method: credentials.method,
64
74
  error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
75
+ ...(credentials.subscriptionOverride ? { subscriptionOverride: credentials.subscriptionOverride } : {}),
65
76
  };
66
77
  }
67
78
 
@@ -80,26 +91,54 @@ export class ClaudeProviderAuth implements IProviderAuth {
80
91
  }
81
92
 
82
93
  /**
83
- * Checks Claude credentials in the same priority order used by Claude Code.
94
+ * Finds an API-key style credential in the same order Claude Code checks them:
95
+ * process env first (auth token, then API key), then the settings.json env block.
84
96
  */
85
- private async checkCredentials(): Promise<ClaudeCredentialsStatus> {
86
- const missingCredentialsError = 'Claude CLI is not authenticated. Run claude /login or configure ANTHROPIC_API_KEY.';
87
-
97
+ private findApiKeyCredential(settingsEnv: Record<string, unknown>): ClaudeApiKeyCredential | null {
88
98
  if (process.env.ANTHROPIC_AUTH_TOKEN?.trim()) {
89
- return { authenticated: true, email: 'Auth Token', method: 'api_key' };
99
+ return { variable: 'ANTHROPIC_AUTH_TOKEN', source: 'process_env', label: 'Auth Token' };
90
100
  }
91
101
 
92
102
  if (process.env.ANTHROPIC_API_KEY?.trim()) {
93
- return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
103
+ return { variable: 'ANTHROPIC_API_KEY', source: 'process_env', label: 'API Key Auth' };
94
104
  }
95
105
 
96
- const settingsEnv = await this.loadSettingsEnv();
97
106
  if (readOptionalString(settingsEnv.ANTHROPIC_API_KEY)) {
98
- return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
107
+ return { variable: 'ANTHROPIC_API_KEY', source: 'settings_file', label: 'API Key Auth' };
99
108
  }
100
109
 
101
110
  if (readOptionalString(settingsEnv.ANTHROPIC_AUTH_TOKEN)) {
102
- return { authenticated: true, email: 'Configured via settings.json', method: 'api_key' };
111
+ return { variable: 'ANTHROPIC_AUTH_TOKEN', source: 'settings_file', label: 'Configured via settings.json' };
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Checks Claude credentials in the same priority order used by Claude Code.
119
+ */
120
+ private async checkCredentials(): Promise<ClaudeCredentialsStatus> {
121
+ const settingsEnv = await this.loadSettingsEnv();
122
+
123
+ const apiKey = this.findApiKeyCredential(settingsEnv);
124
+ if (apiKey) {
125
+ const status: ClaudeCredentialsStatus = { authenticated: true, email: apiKey.label, method: 'api_key' };
126
+
127
+ // The key wins, but Claude Code says nothing when it does so while a
128
+ // `claude /login` subscription is also signed in — every request is then
129
+ // billed pay-as-you-go to the key (issue #568). Surface the bypassed
130
+ // login so the settings page can warn about it; an expired or missing
131
+ // login is not being bypassed, so it stays silent.
132
+ const subscription = await this.readCredentialsFile();
133
+ if (subscription.authenticated) {
134
+ status.subscriptionOverride = {
135
+ variable: apiKey.variable,
136
+ source: apiKey.source,
137
+ subscriptionEmail: subscription.email,
138
+ };
139
+ }
140
+
141
+ return status;
103
142
  }
104
143
 
105
144
  if (process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim()) {
@@ -110,6 +149,17 @@ export class ClaudeProviderAuth implements IProviderAuth {
110
149
  return { authenticated: true, email: 'OAuth Token (long-lived)', method: 'environment' };
111
150
  }
112
151
 
152
+ return this.readCredentialsFile();
153
+ }
154
+
155
+ /**
156
+ * Reads the `claude /login` OAuth session from ~/.claude/.credentials.json and
157
+ * reports whether it is still usable. Shared by the fallback path (when no
158
+ * env credential exists) and by the API-key path (to detect a bypassed login).
159
+ */
160
+ private async readCredentialsFile(): Promise<ClaudeCredentialsStatus> {
161
+ const missingCredentialsError = 'Claude CLI is not authenticated. Run claude /login or configure ANTHROPIC_API_KEY.';
162
+
113
163
  try {
114
164
  const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
115
165
  const content = await readFile(credPath, 'utf8');
@@ -7,7 +7,11 @@ import type {
7
7
  ProviderModelOption,
8
8
  ProviderModelsDefinition,
9
9
  } from '@/shared/types.js';
10
- import { buildDefaultProviderCurrentActiveModel } from '@/shared/utils.js';
10
+ import {
11
+ buildDefaultProviderCurrentActiveModel,
12
+ isPlaceholderProviderModel,
13
+ stripAnsiSequences,
14
+ } from '@/shared/utils.js';
11
15
 
12
16
  /**
13
17
  * Ultracode is not one of the SDK's reasoning-effort levels. Selecting it runs the turn at
@@ -179,20 +183,6 @@ type ClaudeInitEvent = {
179
183
  };
180
184
  };
181
185
 
182
- const ANSI_PATTERN = new RegExp(
183
- '[\\u001B\\u009B][[\\]()#;?]*(?:'
184
- + '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
185
- + '|(?:[\\dA-PR-TZcf-ntqry=><~]))',
186
- 'g',
187
- );
188
-
189
- /**
190
- * Claude Code stamps locally-synthesized rows (API-error placeholders and the
191
- * like) with `model: "<synthetic>"`. Angle-bracketed values are placeholders,
192
- * never real model ids, and must not be surfaced as the session's model.
193
- */
194
- const isPlaceholderModel = (model: string): boolean => model.startsWith('<') && model.endsWith('>');
195
-
196
186
  /** Exported for tests. */
197
187
  export const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: string): string | null => {
198
188
  const eventSessionId = event.sessionId ?? event.session_id;
@@ -206,16 +196,14 @@ export const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: strin
206
196
  }
207
197
 
208
198
  const directModel = event.model?.trim();
209
- if (directModel && !isPlaceholderModel(directModel)) {
199
+ if (directModel && !isPlaceholderProviderModel(directModel)) {
210
200
  return directModel;
211
201
  }
212
202
 
213
203
  const messageModel = event.message?.model?.trim();
214
- return messageModel && !isPlaceholderModel(messageModel) ? messageModel : null;
204
+ return messageModel && !isPlaceholderProviderModel(messageModel) ? messageModel : null;
215
205
  };
216
206
 
217
- const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, '');
218
-
219
207
  const extractTaggedContent = (content: string, tagName: string): string | null => {
220
208
  const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
221
209
  const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
@@ -225,17 +213,17 @@ const extractTaggedContent = (content: string, tagName: string): string | null =
225
213
  const extractClaudeModelFromTextContent = (content: string): string | null => {
226
214
  const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
227
215
  if (localCommandStdout !== null) {
228
- const cleanedStdout = stripAnsi(localCommandStdout).replace(/\s+/g, ' ').trim();
216
+ const cleanedStdout = stripAnsiSequences(localCommandStdout).replace(/\s+/g, ' ').trim();
229
217
  const changedModel = /(?:set|changed|switched)\s+model\s+to\s+(.+?)\.?$/i.exec(cleanedStdout);
230
218
  const stdoutModel = changedModel?.[1]?.trim();
231
219
  // A placeholder stdout hit must not shadow a real <model> tag further down.
232
- if (stdoutModel && !isPlaceholderModel(stdoutModel)) {
220
+ if (stdoutModel && !isPlaceholderProviderModel(stdoutModel)) {
233
221
  return stdoutModel;
234
222
  }
235
223
  }
236
224
 
237
225
  const modelTag = extractTaggedContent(content, 'model')?.trim();
238
- return modelTag && !isPlaceholderModel(modelTag) ? modelTag : null;
226
+ return modelTag && !isPlaceholderProviderModel(modelTag) ? modelTag : null;
239
227
  };
240
228
 
241
229
  const extractClaudeModelFromMessageContent = (content: unknown): string | null => {