@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
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
7
  import { ClaudeProviderAuth } from '@/modules/providers/list/claude/claude-auth.provider.js';
8
+ import type { ProviderAuthStatus, ProviderAuthSubscriptionOverride } from '@/shared/types.js';
8
9
 
9
10
  // checkCredentials() is private, but unlike getStatus() it never shells out to the
10
11
  // `claude` CLI — it only reads env vars and ~/.claude files. Calling it directly
@@ -15,11 +16,29 @@ type CheckCredentialsResult = {
15
16
  email: string | null;
16
17
  method: string | null;
17
18
  error?: string;
19
+ subscriptionOverride?: ProviderAuthSubscriptionOverride;
18
20
  };
19
21
 
20
22
  const checkCredentials = (auth: ClaudeProviderAuth): Promise<CheckCredentialsResult> =>
21
23
  (auth as unknown as { checkCredentials: () => Promise<CheckCredentialsResult> }).checkCredentials();
22
24
 
25
+ // getStatus() is the public API the route serves, but it first shells out to
26
+ // `claude --version`. Stubbing checkInstalled() lets the test cover the mapping
27
+ // from checkCredentials() to the wire shape without the CLI.
28
+ const getStatusWithCliInstalled = (auth: ClaudeProviderAuth): Promise<ProviderAuthStatus> => {
29
+ (auth as unknown as { checkInstalled: () => boolean }).checkInstalled = () => true;
30
+ return auth.getStatus();
31
+ };
32
+
33
+ const validSubscriptionCredentials = (email?: string) => ({
34
+ claudeAiOauth: {
35
+ accessToken: 'valid-token',
36
+ refreshToken: 'live-refresh-token',
37
+ expiresAt: Date.now() + 60 * 60 * 1000,
38
+ },
39
+ ...(email ? { email } : {}),
40
+ });
41
+
23
42
  const ENV_KEYS = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'] as const;
24
43
 
25
44
  const withEnv = async (
@@ -206,3 +225,134 @@ test('checkCredentials: ANTHROPIC_API_KEY takes precedence over CLAUDE_CODE_OAUT
206
225
  );
207
226
  });
208
227
  });
228
+
229
+ // Issue #568: Claude Code silently prefers ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
230
+ // over the `claude /login` subscription, billing every request to the key. The
231
+ // status must say so — and only when a usable login is really being bypassed.
232
+
233
+ test('checkCredentials: ANTHROPIC_API_KEY in the process env alongside a valid subscription login reports the override', async () => {
234
+ await withTempHome(async (homeDir) => {
235
+ await writeCredentialsFile(homeDir, validSubscriptionCredentials('someone@example.com'));
236
+
237
+ await withEnv({ ANTHROPIC_API_KEY: 'test-api-key' }, async () => {
238
+ const status = await checkCredentials(new ClaudeProviderAuth());
239
+ // The existing fields are untouched: the key still wins and keeps its label.
240
+ assert.equal(status.authenticated, true);
241
+ assert.equal(status.method, 'api_key');
242
+ assert.equal(status.email, 'API Key Auth');
243
+ assert.deepEqual(status.subscriptionOverride, {
244
+ variable: 'ANTHROPIC_API_KEY',
245
+ source: 'process_env',
246
+ subscriptionEmail: 'someone@example.com',
247
+ });
248
+ });
249
+ });
250
+ });
251
+
252
+ test('checkCredentials: ANTHROPIC_AUTH_TOKEN in the process env alongside a valid subscription login names the auth token', async () => {
253
+ await withTempHome(async (homeDir) => {
254
+ await writeCredentialsFile(homeDir, validSubscriptionCredentials());
255
+
256
+ await withEnv({ ANTHROPIC_AUTH_TOKEN: 'test-auth-token', ANTHROPIC_API_KEY: 'test-api-key' }, async () => {
257
+ const status = await checkCredentials(new ClaudeProviderAuth());
258
+ assert.equal(status.method, 'api_key');
259
+ assert.equal(status.email, 'Auth Token');
260
+ assert.deepEqual(status.subscriptionOverride, {
261
+ variable: 'ANTHROPIC_AUTH_TOKEN',
262
+ source: 'process_env',
263
+ subscriptionEmail: null,
264
+ });
265
+ });
266
+ });
267
+ });
268
+
269
+ test('checkCredentials: ANTHROPIC_API_KEY in settings.json env alongside a valid subscription login points at settings.json', async () => {
270
+ await withTempHome(async (homeDir) => {
271
+ await writeSettingsFile(homeDir, { ANTHROPIC_API_KEY: 'test-api-key-from-settings' });
272
+ await writeCredentialsFile(homeDir, validSubscriptionCredentials('someone@example.com'));
273
+
274
+ await withEnv({}, async () => {
275
+ const status = await checkCredentials(new ClaudeProviderAuth());
276
+ assert.equal(status.method, 'api_key');
277
+ assert.equal(status.email, 'API Key Auth');
278
+ assert.deepEqual(status.subscriptionOverride, {
279
+ variable: 'ANTHROPIC_API_KEY',
280
+ source: 'settings_file',
281
+ subscriptionEmail: 'someone@example.com',
282
+ });
283
+ });
284
+ });
285
+ });
286
+
287
+ test('checkCredentials: ANTHROPIC_API_KEY with no credentials file reports no override', async () => {
288
+ await withTempHome(async () => {
289
+ await withEnv({ ANTHROPIC_API_KEY: 'test-api-key' }, async () => {
290
+ const status = await checkCredentials(new ClaudeProviderAuth());
291
+ assert.equal(status.authenticated, true);
292
+ assert.equal(status.method, 'api_key');
293
+ assert.equal(status.subscriptionOverride, undefined);
294
+ });
295
+ });
296
+ });
297
+
298
+ test('checkCredentials: ANTHROPIC_API_KEY with an expired subscription login reports no override', async () => {
299
+ await withTempHome(async (homeDir) => {
300
+ // Nothing usable is being bypassed here, so warning would only confuse.
301
+ await writeCredentialsFile(homeDir, {
302
+ claudeAiOauth: {
303
+ accessToken: 'stale-token',
304
+ refreshToken: 'stale-refresh-token',
305
+ expiresAt: 1_000_000_000_000,
306
+ refreshTokenExpiresAt: 1_000_000_000_000,
307
+ },
308
+ });
309
+
310
+ await withEnv({ ANTHROPIC_API_KEY: 'test-api-key' }, async () => {
311
+ const status = await checkCredentials(new ClaudeProviderAuth());
312
+ assert.equal(status.authenticated, true);
313
+ assert.equal(status.method, 'api_key');
314
+ assert.equal(status.subscriptionOverride, undefined);
315
+ });
316
+ });
317
+ });
318
+
319
+ test('checkCredentials: a valid subscription login with no API key reports credentials_file and no override', async () => {
320
+ await withTempHome(async (homeDir) => {
321
+ await writeCredentialsFile(homeDir, validSubscriptionCredentials('someone@example.com'));
322
+
323
+ await withEnv({}, async () => {
324
+ const status = await checkCredentials(new ClaudeProviderAuth());
325
+ assert.equal(status.authenticated, true);
326
+ assert.equal(status.method, 'credentials_file');
327
+ assert.equal(status.email, 'someone@example.com');
328
+ assert.equal(status.subscriptionOverride, undefined);
329
+ });
330
+ });
331
+ });
332
+
333
+ test('getStatus: passes the subscription override through to the wire shape, and omits the key when there is none', async () => {
334
+ await withTempHome(async (homeDir) => {
335
+ await writeCredentialsFile(homeDir, validSubscriptionCredentials('someone@example.com'));
336
+
337
+ await withEnv({ ANTHROPIC_API_KEY: 'test-api-key' }, async () => {
338
+ const status = await getStatusWithCliInstalled(new ClaudeProviderAuth());
339
+ assert.equal(status.installed, true);
340
+ assert.equal(status.provider, 'claude');
341
+ assert.equal(status.authenticated, true);
342
+ assert.equal(status.method, 'api_key');
343
+ assert.equal(status.email, 'API Key Auth');
344
+ assert.deepEqual(status.subscriptionOverride, {
345
+ variable: 'ANTHROPIC_API_KEY',
346
+ source: 'process_env',
347
+ subscriptionEmail: 'someone@example.com',
348
+ });
349
+ });
350
+
351
+ await withEnv({}, async () => {
352
+ const status = await getStatusWithCliInstalled(new ClaudeProviderAuth());
353
+ assert.equal(status.method, 'credentials_file');
354
+ // Omitted, not null: existing JSON consumers see exactly the old payload.
355
+ assert.equal('subscriptionOverride' in status, false);
356
+ });
357
+ });
358
+ });
@@ -0,0 +1,213 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ createBackgroundWorkTracker,
6
+ startsBackgroundWork,
7
+ } from '@/modules/providers/list/claude/claude-runtime.provider.js';
8
+
9
+ // Only turns that start work outliving the turn hold their CLI process open. A
10
+ // turn scored `false` here has its stdin released the moment `result` arrives,
11
+ // and the CLI reads that EOF as print wind-down — so anything still running dies.
12
+ const turn = (...blocks: Array<{ name: string; input?: Record<string, unknown> }>) => ({
13
+ type: 'assistant',
14
+ message: { content: blocks.map((block) => ({ type: 'tool_use', input: {}, ...block })) },
15
+ });
16
+
17
+ test('a backgrounded Bash holds the process open', () => {
18
+ assert.equal(startsBackgroundWork(turn({ name: 'Bash', input: { run_in_background: true } })), true);
19
+ });
20
+
21
+ test('a foreground Bash does not', () => {
22
+ assert.equal(startsBackgroundWork(turn({ name: 'Bash', input: { run_in_background: false } })), false);
23
+ });
24
+
25
+ test('a backgrounded Agent holds the process open', () => {
26
+ // The gap this suite exists for: a background agent used to be scored as
27
+ // nothing outstanding, so the CLI wound down and killed it mid-run.
28
+ assert.equal(startsBackgroundWork(turn({ name: 'Agent', input: { run_in_background: true } })), true);
29
+ });
30
+
31
+ test('an Agent with no run_in_background holds the process open', () => {
32
+ // `run_in_background` is optional on AgentInput and agents background by
33
+ // default, so an omitted field means background, not foreground.
34
+ assert.equal(startsBackgroundWork(turn({ name: 'Agent', input: { prompt: 'Investigate' } })), true);
35
+ });
36
+
37
+ test('a foreground Agent does not', () => {
38
+ // It never pushes a follow-up turn, so holding for it would pin the process
39
+ // for the full BG_WAIT_CEILING_MS.
40
+ assert.equal(startsBackgroundWork(turn({ name: 'Agent', input: { run_in_background: false } })), false);
41
+ });
42
+
43
+ test('a Workflow holds the process open', () => {
44
+ // WorkflowInput has no foreground option: every call returns a task id
45
+ // immediately and reports back in a later turn.
46
+ assert.equal(startsBackgroundWork(turn({ name: 'Workflow', input: { script: 'export const meta = {}' } })), true);
47
+ });
48
+
49
+ test('a deferred-work tool holds the process open', () => {
50
+ assert.equal(startsBackgroundWork(turn({ name: 'Monitor' })), true);
51
+ });
52
+
53
+ test('a turn that starts nothing lasting does not', () => {
54
+ assert.equal(startsBackgroundWork(turn({ name: 'Read' })), false);
55
+ });
56
+
57
+ test('one backgrounded agent among foreground calls is enough', () => {
58
+ assert.equal(
59
+ startsBackgroundWork(
60
+ turn(
61
+ { name: 'Read' },
62
+ { name: 'Agent', input: { run_in_background: false } },
63
+ { name: 'Agent', input: { run_in_background: true } },
64
+ ),
65
+ ),
66
+ true,
67
+ );
68
+ });
69
+
70
+ test('a message carrying no tool calls does not', () => {
71
+ assert.equal(startsBackgroundWork({ type: 'result', message: { content: 'done' } }), false);
72
+ assert.equal(startsBackgroundWork({ type: 'assistant', message: { content: [] } }), false);
73
+ assert.equal(startsBackgroundWork({}), false);
74
+ });
75
+
76
+ // Once the turn's `result` is out, the tracker below is the only record of
77
+ // what the held process is still running: the running-sessions list reads it
78
+ // and a stop request is refused unless it names one of its tasks. The event
79
+ // shapes are the SDK's own, as a real query emits them.
80
+ const started = (taskId: string, extra: Record<string, unknown> = {}) => ({
81
+ type: 'system',
82
+ subtype: 'task_started',
83
+ task_id: taskId,
84
+ tool_use_id: `toolu_${taskId}`,
85
+ description: `Task ${taskId}`,
86
+ task_type: 'local_agent',
87
+ ...extra,
88
+ });
89
+ const notified = (taskId: string, status: string) => ({
90
+ type: 'system', subtype: 'task_notification', task_id: taskId, tool_use_id: `toolu_${taskId}`, status, summary: '', output_file: '',
91
+ });
92
+ const updated = (taskId: string, patch: Record<string, unknown>) => ({
93
+ type: 'system', subtype: 'task_updated', task_id: taskId, patch,
94
+ });
95
+
96
+ test('a task_started with a tool_use_id adds the task under its session', () => {
97
+ const tracker = createBackgroundWorkTracker();
98
+ const before = Date.now();
99
+ // The session's own turn issued the call, so the task is its own work.
100
+ tracker.apply('s1', {
101
+ type: 'assistant',
102
+ parent_tool_use_id: null,
103
+ message: { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_t1', name: 'Workflow', input: {} }] },
104
+ });
105
+ tracker.apply('s1', started('t1', { task_type: 'local_workflow', workflow_name: 'spec' }));
106
+
107
+ const [entry] = tracker.list();
108
+ assert.equal(tracker.list().length, 1);
109
+ assert.equal(entry.sessionId, 's1');
110
+ assert.equal(entry.tasks.length, 1);
111
+ const { startedAt, ...task } = entry.tasks[0];
112
+ assert.deepEqual(task, {
113
+ taskId: 't1',
114
+ toolUseId: 'toolu_t1',
115
+ taskType: 'local_workflow',
116
+ description: 'Task t1',
117
+ workflowName: 'spec',
118
+ });
119
+ assert.ok(startedAt >= before && startedAt <= Date.now());
120
+ assert.equal(tracker.hasOutstanding('s1'), true);
121
+ assert.equal(tracker.has('s1', 't1'), true);
122
+ });
123
+
124
+ test('a task started for a call the session itself made is its own; one from inside an agent is nested', () => {
125
+ // Verified on a real run: a workflow agent that backgrounds `sleep 60` puts
126
+ // a `task_started` on the parent's stream, with a tool_use_id from the
127
+ // agent's transcript. The parent transcript has no card for it, and the
128
+ // pill would otherwise read "3 tasks" for one workflow.
129
+ const tracker = createBackgroundWorkTracker();
130
+ tracker.apply('s1', {
131
+ type: 'assistant',
132
+ parent_tool_use_id: null,
133
+ message: { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_t1', name: 'Workflow', input: {} }] },
134
+ });
135
+ tracker.apply('s1', started('t1', { task_type: 'local_workflow', workflow_name: 'spec' }));
136
+ tracker.apply('s1', started('t2', { task_type: 'local_bash', description: 'Sleep for 60 seconds' }));
137
+
138
+ const [entry] = tracker.list();
139
+ assert.deepEqual(entry.tasks.map((task) => [task.taskId, task.nested ?? false]), [['t1', false], ['t2', true]]);
140
+ assert.equal(tracker.has('s1', 't2'), true, 'a nested task can still be stopped');
141
+
142
+ // The set of own calls dies with the session, like the tasks.
143
+ tracker.clear('s1');
144
+ tracker.apply('s1', started('t1', { task_type: 'local_workflow' }));
145
+ assert.equal(tracker.list()[0].tasks[0].nested, true);
146
+ });
147
+
148
+ test('a task_started without a tool_use_id is not tracked', () => {
149
+ // Housekeeping tasks the CLI starts on its own have no launching call and
150
+ // nothing in the transcript to show them under.
151
+ const tracker = createBackgroundWorkTracker();
152
+ tracker.apply('s1', { type: 'system', subtype: 'task_started', task_id: 'ambient', description: 'x' });
153
+
154
+ assert.deepEqual(tracker.list(), []);
155
+ assert.equal(tracker.hasOutstanding('s1'), false);
156
+ });
157
+
158
+ test('a task_notification removes the task whatever its status', () => {
159
+ const tracker = createBackgroundWorkTracker();
160
+ tracker.apply('s1', started('t1'));
161
+ tracker.apply('s1', started('t2'));
162
+ tracker.apply('s1', notified('t1', 'completed'));
163
+
164
+ assert.equal(tracker.has('s1', 't1'), false);
165
+ assert.equal(tracker.has('s1', 't2'), true);
166
+ assert.equal(tracker.hasOutstanding('s1'), true);
167
+
168
+ tracker.apply('s1', notified('t2', 'stopped'));
169
+ assert.equal(tracker.hasOutstanding('s1'), false);
170
+ assert.deepEqual(tracker.list(), []);
171
+ });
172
+
173
+ test('a terminal task_updated removes the task', () => {
174
+ const tracker = createBackgroundWorkTracker();
175
+ for (const [taskId, status] of [['a', 'completed'], ['b', 'failed'], ['c', 'killed']]) {
176
+ tracker.apply('s1', started(taskId));
177
+ tracker.apply('s1', updated(taskId, { status, end_time: 1 }));
178
+ assert.equal(tracker.has('s1', taskId), false, `${status} settles the task`);
179
+ }
180
+ assert.equal(tracker.hasOutstanding('s1'), false);
181
+ });
182
+
183
+ test('a non-terminal task_updated keeps the task', () => {
184
+ const tracker = createBackgroundWorkTracker();
185
+ tracker.apply('s1', started('t1'));
186
+ tracker.apply('s1', updated('t1', { status: 'running' }));
187
+ tracker.apply('s1', updated('t1', { is_backgrounded: true }));
188
+ tracker.apply('s1', updated('t1', { status: 'paused' }));
189
+
190
+ assert.equal(tracker.has('s1', 't1'), true);
191
+ assert.equal(tracker.hasOutstanding('s1'), true);
192
+ });
193
+
194
+ test('clearing a session empties it and leaves the others alone', () => {
195
+ const tracker = createBackgroundWorkTracker();
196
+ tracker.apply('s1', started('t1'));
197
+ tracker.apply('s2', started('t2'));
198
+ tracker.clear('s1');
199
+
200
+ assert.equal(tracker.hasOutstanding('s1'), false);
201
+ assert.equal(tracker.has('s1', 't1'), false);
202
+ assert.deepEqual(tracker.list().map((entry) => entry.sessionId), ['s2']);
203
+ });
204
+
205
+ test('messages that are not task events leave the set untouched', () => {
206
+ const tracker = createBackgroundWorkTracker();
207
+ tracker.apply('s1', started('t1'));
208
+ tracker.apply('s1', { type: 'assistant', message: { content: [] } });
209
+ tracker.apply('s1', { type: 'system', subtype: 'task_progress', task_id: 't1', description: 'x', usage: {} });
210
+ tracker.apply('s1', { type: 'result', task_id: 't1' });
211
+
212
+ assert.equal(tracker.has('s1', 't1'), true);
213
+ });
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
5
+
6
+ const provider = new ClaudeSessionsProvider();
7
+ const SESSION_ID = 'claude-compaction-1';
8
+
9
+ /**
10
+ * Both records that describe a compaction are `system` events, which normalized
11
+ * to nothing before this: the boundary carries the numbers, the status says one
12
+ * is running. Each becomes one ordinary assistant row, so a client that knows
13
+ * nothing of `compact` still reads the sentence.
14
+ */
15
+ test('a compact boundary becomes one row carrying its numbers', () => {
16
+ const [row, ...rest] = provider.normalizeMessage({
17
+ type: 'system',
18
+ subtype: 'compact_boundary',
19
+ uuid: 'u-1',
20
+ timestamp: '2026-01-01T00:00:00.000Z',
21
+ compactMetadata: {
22
+ trigger: 'manual',
23
+ preTokens: 335_402,
24
+ postTokens: 10_120,
25
+ durationMs: 142_000,
26
+ },
27
+ }, SESSION_ID);
28
+
29
+ assert.equal(rest.length, 0);
30
+ assert.equal(row.kind, 'text');
31
+ assert.equal(row.role, 'assistant');
32
+ assert.equal(row.content, 'Compacted · manual · 335k → 10k tokens · 2m 22s');
33
+ assert.deepEqual(row.compact, {
34
+ phase: 'done',
35
+ trigger: 'manual',
36
+ preTokens: 335_402,
37
+ postTokens: 10_120,
38
+ durationMs: 142_000,
39
+ });
40
+ });
41
+
42
+ test('the live spelling of the metadata reads the same', () => {
43
+ const [row] = provider.normalizeMessage({
44
+ type: 'system',
45
+ subtype: 'compact_boundary',
46
+ compact_metadata: { trigger: 'auto', pre_tokens: 1_240_000, post_tokens: 18_000, duration_ms: 45_000 },
47
+ }, SESSION_ID);
48
+
49
+ assert.equal(row.content, 'Compacted · auto · 1.2M → 18k tokens · 45s');
50
+ assert.equal(row.compact?.trigger, 'auto');
51
+ });
52
+
53
+ test('a boundary with no metadata still says a compaction happened', () => {
54
+ const [row] = provider.normalizeMessage({
55
+ type: 'system',
56
+ subtype: 'compact_boundary',
57
+ }, SESSION_ID);
58
+
59
+ assert.equal(row.content, 'Compacted · auto');
60
+ assert.equal(row.compact?.phase, 'done');
61
+ });
62
+
63
+ test('a compacting status becomes a running row, and other statuses none', () => {
64
+ const [running] = provider.normalizeMessage({
65
+ type: 'system',
66
+ subtype: 'status',
67
+ status: 'compacting',
68
+ }, SESSION_ID);
69
+
70
+ assert.equal(running.content, 'Compacting conversation…');
71
+ assert.equal(running.compact?.phase, 'running');
72
+
73
+ assert.deepEqual(
74
+ provider.normalizeMessage({ type: 'system', subtype: 'status', status: 'requesting' }, SESSION_ID),
75
+ [],
76
+ );
77
+ });
78
+
79
+ test('a failed compaction says so, with the reason when there is one', () => {
80
+ const [failed] = provider.normalizeMessage({
81
+ type: 'system',
82
+ subtype: 'status',
83
+ compact_result: 'failed',
84
+ compact_error: 'context still too large',
85
+ }, SESSION_ID);
86
+
87
+ assert.equal(failed.content, 'Compaction failed: context still too large');
88
+ assert.equal(failed.compact?.phase, 'failed');
89
+ assert.equal(failed.compact?.error, 'context still too large');
90
+ });
91
+
92
+ test('the summary keeps its flag, so the client can fold it into the row', () => {
93
+ const [row] = provider.normalizeMessage({
94
+ type: 'user',
95
+ isCompactSummary: true,
96
+ message: { role: 'user', content: 'This session is being continued from a previous conversation.' },
97
+ }, SESSION_ID);
98
+
99
+ assert.equal(row.role, 'assistant');
100
+ assert.equal(row.isCompactSummary, true);
101
+ });
@@ -0,0 +1,160 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
8
+ import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
9
+
10
+ const SESSION_ID = 'claude-model-session';
11
+
12
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
13
+ const previousDatabasePath = process.env.DATABASE_PATH;
14
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'claude-model-db-'));
15
+
16
+ closeConnection();
17
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
18
+ await initializeDatabase();
19
+
20
+ try {
21
+ await runTest();
22
+ } finally {
23
+ closeConnection();
24
+ if (previousDatabasePath === undefined) {
25
+ delete process.env.DATABASE_PATH;
26
+ } else {
27
+ process.env.DATABASE_PATH = previousDatabasePath;
28
+ }
29
+ await rm(tempDirectory, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * One assistant transcript row in the shape Claude Code writes it: the model
35
+ * that answered sits on `message.model`, beside the content parts.
36
+ */
37
+ const assistantRow = (uuid: string, model: string | undefined, content: unknown) => ({
38
+ parentUuid: 'user-1',
39
+ isSidechain: false,
40
+ type: 'assistant',
41
+ uuid,
42
+ timestamp: '2026-07-10T00:00:01.000Z',
43
+ cwd: '/workspace/demo',
44
+ sessionId: SESSION_ID,
45
+ message: { role: 'assistant', ...(model === undefined ? {} : { model }), content },
46
+ });
47
+
48
+ test('an assistant reply carries the model that answered it', () => {
49
+ const messages = new ClaudeSessionsProvider().normalizeMessage(
50
+ assistantRow('asst-1', 'claude-opus-5', [
51
+ { type: 'thinking', thinking: 'weighing the options' },
52
+ { type: 'text', text: 'Here is the plan.' },
53
+ { type: 'tool_use', id: 'toolu_1', name: 'Read', input: { file_path: '/repo/a.ts' } },
54
+ ]),
55
+ SESSION_ID,
56
+ );
57
+
58
+ const prose = messages.find((message) => message.kind === 'text');
59
+ assert.equal(prose?.model, 'claude-opus-5');
60
+
61
+ // Only prose has a footer to show it in; a tool call and a reasoning block
62
+ // are drawn as cards, so stamping them would carry a model nothing renders.
63
+ assert.equal(messages.find((message) => message.kind === 'tool_use')?.model, undefined);
64
+ assert.equal(messages.find((message) => message.kind === 'thinking')?.model, undefined);
65
+ });
66
+
67
+ test('an assistant reply whose content is a bare string carries the model too', () => {
68
+ const [prose] = new ClaudeSessionsProvider().normalizeMessage(
69
+ assistantRow('asst-2', 'claude-haiku-4-5-20251001', 'Done.'),
70
+ SESSION_ID,
71
+ );
72
+
73
+ assert.equal(prose?.kind, 'text');
74
+ assert.equal(prose?.model, 'claude-haiku-4-5-20251001');
75
+ });
76
+
77
+ test('the reported model id is passed through verbatim, suffix and all', () => {
78
+ // Claude Code writes the base id on assistant rows today, even in 1M-context
79
+ // sessions. The normalizer must still never rewrite what the provider wrote:
80
+ // if a `[1m]` id ever appears, it has to reach the client as itself.
81
+ const [prose] = new ClaudeSessionsProvider().normalizeMessage(
82
+ assistantRow('asst-3', 'claude-opus-5[1m]', [{ type: 'text', text: 'Reading the whole repo.' }]),
83
+ SESSION_ID,
84
+ );
85
+
86
+ assert.equal(prose?.model, 'claude-opus-5[1m]');
87
+ });
88
+
89
+ test('a locally fabricated row reports no model at all', () => {
90
+ // Claude Code stamps rows it synthesized itself — the usage-limit notice, an
91
+ // API-error placeholder — with `<synthetic>`. No request ran, so there is no
92
+ // model to name, and a guessed one would be a lie about what was charged.
93
+ const [prose] = new ClaudeSessionsProvider().normalizeMessage(
94
+ assistantRow('asst-4', '<synthetic>', [{ type: 'text', text: 'Claude usage limit reached.' }]),
95
+ SESSION_ID,
96
+ );
97
+
98
+ assert.equal(prose?.kind, 'text');
99
+ assert.equal(prose?.model, undefined);
100
+ });
101
+
102
+ test('a user turn carries no model, because no transcript records one', () => {
103
+ const [turn] = new ClaudeSessionsProvider().normalizeMessage(
104
+ {
105
+ type: 'user',
106
+ uuid: 'user-1',
107
+ timestamp: '2026-07-10T00:00:00.000Z',
108
+ sessionId: SESSION_ID,
109
+ message: { role: 'user', content: 'Plan the refactor.' },
110
+ },
111
+ SESSION_ID,
112
+ );
113
+
114
+ assert.equal(turn?.role, 'user');
115
+ assert.equal(turn?.model, undefined);
116
+ });
117
+
118
+ test('Claude history replays the model of every assistant turn from the transcript', { concurrency: false }, async () => {
119
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'claude-model-history-'));
120
+
121
+ try {
122
+ const transcriptPath = path.join(tempRoot, `${SESSION_ID}.jsonl`);
123
+ await writeFile(
124
+ transcriptPath,
125
+ [
126
+ JSON.stringify({
127
+ parentUuid: null,
128
+ isSidechain: false,
129
+ type: 'user',
130
+ uuid: 'user-1',
131
+ timestamp: '2026-07-10T00:00:00.000Z',
132
+ cwd: '/workspace/demo',
133
+ sessionId: SESSION_ID,
134
+ message: { role: 'user', content: 'Plan the refactor.' },
135
+ }),
136
+ JSON.stringify(assistantRow('asst-1', 'claude-opus-5', [{ type: 'text', text: 'Here is the plan.' }])),
137
+ JSON.stringify(assistantRow('asst-2', '<synthetic>', [{ type: 'text', text: 'Claude usage limit reached.' }])),
138
+ '',
139
+ ].join('\n'),
140
+ 'utf8',
141
+ );
142
+
143
+ await withIsolatedDatabase(async () => {
144
+ const now = new Date().toISOString();
145
+ sessionsDb.createSession(SESSION_ID, 'claude', tempRoot, 'Model session', now, now, transcriptPath);
146
+
147
+ const history = await new ClaudeSessionsProvider().fetchHistory(SESSION_ID, {
148
+ providerSessionId: SESSION_ID,
149
+ });
150
+
151
+ const replies = history.messages.filter((message) => message.kind === 'text' && message.role === 'assistant');
152
+ assert.equal(replies.length, 2);
153
+ assert.equal(replies[0]?.model, 'claude-opus-5');
154
+ assert.equal(replies[1]?.model, undefined);
155
+ assert.equal(history.messages.find((message) => message.role === 'user')?.model, undefined);
156
+ });
157
+ } finally {
158
+ await rm(tempRoot, { recursive: true, force: true });
159
+ }
160
+ });