@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
@@ -0,0 +1,348 @@
1
+ import assert from 'node:assert/strict';
2
+ import * as nodeCrypto from 'node:crypto';
3
+ import { once } from 'node:events';
4
+ import { mkdtemp, rm } from 'node:fs/promises';
5
+ import type { AddressInfo } from 'node:net';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import test from 'node:test';
9
+
10
+ import express from 'express';
11
+
12
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
13
+ import { chatRunRegistry, connectedClients } from '@/modules/websocket/index.js';
14
+ import type { NormalizedMessage } from '@/shared/types.js';
15
+ import { createNormalizedMessage } from '@/shared/utils.js';
16
+
17
+ import { createAgentRouter } from '../agent.routes.js';
18
+
19
+ /**
20
+ * `POST /api/agent` runs a provider for an API-key caller. Until now it wrote
21
+ * the run's events straight to the HTTP response and nothing else knew: the
22
+ * session was not on the running-sessions list, no tab could subscribe to
23
+ * it, and a second request could start a second run on the same session. It
24
+ * now registers the run with the chat run registry the way a `chat.send`
25
+ * does, with the HTTP response as the run's first audience.
26
+ */
27
+
28
+ type AgentDependencies = Parameters<typeof createAgentRouter>[0];
29
+ type RunFunction = AgentDependencies['queryClaude'];
30
+
31
+ async function withIsolatedDatabase(run: () => Promise<void>): Promise<void> {
32
+ const previousDatabasePath = process.env.DATABASE_PATH;
33
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'agent-run-registration-'));
34
+ closeConnection();
35
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
36
+ await initializeDatabase();
37
+ try {
38
+ await run();
39
+ } finally {
40
+ connectedClients.clear();
41
+ chatRunRegistry.clearAll();
42
+ closeConnection();
43
+ if (previousDatabasePath === undefined) {
44
+ delete process.env.DATABASE_PATH;
45
+ } else {
46
+ process.env.DATABASE_PATH = previousDatabasePath;
47
+ }
48
+ await rm(tempDirectory, { recursive: true, force: true });
49
+ }
50
+ }
51
+
52
+ function createDependencies(queryClaude: RunFunction): AgentDependencies {
53
+ const unexpected = async (): Promise<never> => { throw new Error('unexpected provider call'); };
54
+ return {
55
+ fileSystem: { access: async () => undefined } as unknown as AgentDependencies['fileSystem'],
56
+ crypto: nodeCrypto,
57
+ homeDirectory: () => '/home/test',
58
+ spawnProcess: (() => { throw new Error('spawn should not run'); }) as unknown as AgentDependencies['spawnProcess'],
59
+ platformMode: true,
60
+ users: { getFirstUser: () => ({ id: 1, username: 'test-user' }) },
61
+ apiKeys: { validateApiKey: () => undefined },
62
+ githubTokens: { getActiveGithubToken: () => null },
63
+ projects: { createProjectPath: () => ({ outcome: 'created' }) },
64
+ models: { getProviderModels: async () => ({ OPTIONS: [], DEFAULT: 'default-model' }) } as unknown as AgentDependencies['models'],
65
+ sessions: {
66
+ getSessionById: (sessionId) => sessionsDb.getSessionById(sessionId),
67
+ getSessionByProviderSessionId: (providerSessionId) => sessionsDb.getSessionByProviderSessionId(providerSessionId),
68
+ createAppSession: (provider, projectPath, initialMessage) => {
69
+ const sessionId = `app-${initialMessage.length}-${nodeCrypto.randomUUID()}`;
70
+ sessionsDb.createAppSession(sessionId, provider, projectPath, initialMessage);
71
+ return { sessionId };
72
+ },
73
+ },
74
+ runs: chatRunRegistry,
75
+ queryClaude,
76
+ queryCursor: unexpected as RunFunction,
77
+ queryCodex: unexpected as RunFunction,
78
+ queryOpenCode: unexpected as RunFunction,
79
+ GithubClient: class {} as unknown as AgentDependencies['GithubClient'],
80
+ };
81
+ }
82
+
83
+ async function withAgentServer(dependencies: AgentDependencies, run: (baseUrl: string) => Promise<void>): Promise<void> {
84
+ const app = express();
85
+ app.use(express.json());
86
+ app.use('/api/agent', createAgentRouter(dependencies));
87
+ const server = app.listen(0, '127.0.0.1');
88
+ await once(server, 'listening');
89
+ try {
90
+ const address = server.address() as AddressInfo;
91
+ await run(`http://127.0.0.1:${address.port}`);
92
+ } finally {
93
+ await new Promise<void>((resolve) => server.close(() => resolve()));
94
+ }
95
+ }
96
+
97
+ const post = (baseUrl: string, body: Record<string, unknown>) =>
98
+ fetch(`${baseUrl}/api/agent`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
99
+
100
+ const readEvents = async (response: Response): Promise<Array<Record<string, unknown>>> =>
101
+ (await response.text())
102
+ .split('\n\n')
103
+ .filter((frame) => frame.startsWith('data: '))
104
+ .map((frame) => JSON.parse(frame.slice('data: '.length)) as Record<string, unknown>);
105
+
106
+ /** A runtime that announces its native session id, streams one text event, and waits to be released before completing. */
107
+ function createHeldRuntime(nativeSessionId = 'native-1') {
108
+ let release: () => void = () => undefined;
109
+ const released = new Promise<void>((resolve) => { release = resolve; });
110
+ const seen: Array<{ sessionId: unknown; writerIsGateway: boolean }> = [];
111
+ const queryClaude: RunFunction = async (_command, options, writer) => {
112
+ seen.push({ sessionId: (options as { sessionId?: unknown }).sessionId, writerIsGateway: Boolean((writer as { isWebSocketWriter?: boolean }).isWebSocketWriter) });
113
+ // A resumed session announces the id it resumed under; a new one, a fresh id.
114
+ const announced = (writer as { getSessionId?: () => string | null }).getSessionId?.() ?? nativeSessionId;
115
+ writer.send(createNormalizedMessage({ kind: 'session_created', provider: 'claude', sessionId: announced, newSessionId: announced }));
116
+ writer.send(createNormalizedMessage({ kind: 'text', provider: 'claude', sessionId: announced, role: 'assistant', content: 'hello' }));
117
+ await released;
118
+ writer.send(createNormalizedMessage({ kind: 'complete', provider: 'claude', sessionId: announced, exitCode: 0 }));
119
+ };
120
+ return { queryClaude, release, seen };
121
+ }
122
+
123
+ test('an API run is registered like a chat send: listed while running, streamed decorated, mapped to its native id', async () => {
124
+ await withIsolatedDatabase(async () => {
125
+ const runtime = createHeldRuntime();
126
+ await withAgentServer(createDependencies(runtime.queryClaude), async (baseUrl) => {
127
+ const responsePromise = post(baseUrl, { projectPath: '/home/test/project', message: 'Run it' });
128
+ // Wait for the runtime to be entered.
129
+ for (let i = 0; i < 50 && runtime.seen.length === 0; i += 1) {
130
+ await new Promise((resolve) => { setTimeout(resolve, 20); });
131
+ }
132
+
133
+ const [running] = chatRunRegistry.listRunningRuns();
134
+ assert.ok(running, 'the run is on the running-sessions list while the provider works');
135
+ assert.equal(running.provider, 'claude');
136
+ assert.match(running.sessionId, /^app-/, 'listed under the app session id the route allocated');
137
+ assert.deepEqual(runtime.seen, [{ sessionId: running.sessionId, writerIsGateway: true }], 'the runtime gets the app id and the run\'s gateway writer');
138
+ // A tab that opens the session mid-run replays what it missed.
139
+ assert.deepEqual(chatRunRegistry.replayEvents(running.sessionId, 0).map((event) => event.kind), ['text']);
140
+
141
+ runtime.release();
142
+ const response = await responsePromise;
143
+ assert.equal(response.status, 200);
144
+ assert.match(response.headers.get('content-type') ?? '', /^text\/event-stream/);
145
+ const events = await readEvents(response);
146
+ assert.deepEqual(events.map((event) => event.type ?? event.kind), ['status', 'session-id', 'text', 'complete', 'done']);
147
+ assert.equal(events[1]?.sessionId, running.sessionId, 'session-id names the app session, not the provider\'s');
148
+ // Provider events reach the stream as a tab would see them: app session id, sequenced, no session_created.
149
+ const text = events[2] as NormalizedMessage;
150
+ assert.equal(text.sessionId, running.sessionId);
151
+ assert.equal(text.seq, 1);
152
+
153
+ assert.equal(chatRunRegistry.isProcessing(running.sessionId), false, 'the terminal complete ends the run');
154
+ assert.equal(sessionsDb.getSessionById(running.sessionId)?.provider_session_id, 'native-1', 'the native id the runtime announced is mapped onto the row');
155
+ });
156
+ });
157
+ });
158
+
159
+ test('a second API request on a session mid-run is refused with 409, as the chat socket refuses it', async () => {
160
+ await withIsolatedDatabase(async () => {
161
+ const runtime = createHeldRuntime();
162
+ await withAgentServer(createDependencies(runtime.queryClaude), async (baseUrl) => {
163
+ const first = post(baseUrl, { projectPath: '/home/test/project', message: 'Run it' });
164
+ for (let i = 0; i < 50 && runtime.seen.length === 0; i += 1) {
165
+ await new Promise((resolve) => { setTimeout(resolve, 20); });
166
+ }
167
+ const [running] = chatRunRegistry.listRunningRuns();
168
+ assert.ok(running);
169
+
170
+ const second = await post(baseUrl, { projectPath: '/home/test/project', message: 'Again', sessionId: running.sessionId, stream: false });
171
+ assert.equal(second.status, 409);
172
+ assert.equal(runtime.seen.length, 1, 'the provider is not entered twice');
173
+
174
+ runtime.release();
175
+ await first;
176
+ });
177
+ });
178
+ });
179
+
180
+ test('a non-streaming API run answers with the app session id and is off the list once done', async () => {
181
+ await withIsolatedDatabase(async () => {
182
+ const runtime = createHeldRuntime();
183
+ runtime.release();
184
+ await withAgentServer(createDependencies(runtime.queryClaude), async (baseUrl) => {
185
+ const response = await post(baseUrl, { projectPath: '/home/test/project', message: 'Run it', stream: false });
186
+ assert.equal(response.status, 200);
187
+ const body = await response.json() as { success: boolean; sessionId: string };
188
+ assert.equal(body.success, true);
189
+ assert.match(body.sessionId, /^app-/);
190
+ assert.deepEqual(chatRunRegistry.listRunningRuns(), []);
191
+ });
192
+ });
193
+ });
194
+
195
+ test('an API run continues a session the caller names by either id, and refuses one that does not exist', async () => {
196
+ await withIsolatedDatabase(async () => {
197
+ sessionsDb.createAppSession('app-existing', 'claude', '/home/test/project', 'Existing');
198
+ sessionsDb.assignProviderSessionId('app-existing', 'native-existing');
199
+ const runtime = createHeldRuntime();
200
+ runtime.release();
201
+ await withAgentServer(createDependencies(runtime.queryClaude), async (baseUrl) => {
202
+ const byAppId = await post(baseUrl, { projectPath: '/home/test/project', message: 'More', sessionId: 'app-existing', stream: false });
203
+ assert.equal(byAppId.status, 200);
204
+ assert.equal((await byAppId.json() as { sessionId: string }).sessionId, 'app-existing');
205
+
206
+ // A caller that stored the provider-native id an earlier response gave it.
207
+ const byNativeId = await post(baseUrl, { projectPath: '/home/test/project', message: 'More', sessionId: 'native-existing', stream: false });
208
+ assert.equal(byNativeId.status, 200);
209
+ assert.equal((await byNativeId.json() as { sessionId: string }).sessionId, 'app-existing');
210
+ assert.deepEqual(runtime.seen.map((call) => call.sessionId), ['app-existing', 'app-existing']);
211
+
212
+ const unknown = await post(baseUrl, { projectPath: '/home/test/project', message: 'More', sessionId: 'nope', stream: false });
213
+ assert.equal(unknown.status, 404);
214
+ assert.equal(runtime.seen.length, 2);
215
+ });
216
+ });
217
+ });
218
+
219
+ test('a runtime that throws leaves no run behind', async () => {
220
+ await withIsolatedDatabase(async () => {
221
+ const queryClaude: RunFunction = async () => { throw new Error('provider exploded'); };
222
+ await withAgentServer(createDependencies(queryClaude), async (baseUrl) => {
223
+ const response = await post(baseUrl, { projectPath: '/home/test/project', message: 'Run it', stream: false });
224
+ assert.equal(response.status, 500);
225
+ assert.deepEqual(chatRunRegistry.listRunningRuns(), [], 'the safety net completes the run');
226
+ });
227
+ });
228
+ });
229
+
230
+ test('a sessionId that is not a string is refused, not bound to a query', async () => {
231
+ // The lookup runs before the handler's try: a throw there is an unhandled
232
+ // rejection, and better-sqlite3 throws on binding an object.
233
+ await withIsolatedDatabase(async () => {
234
+ const runtime = createHeldRuntime();
235
+ await withAgentServer(createDependencies(runtime.queryClaude), async (baseUrl) => {
236
+ const response = await post(baseUrl, { projectPath: '/home/test/project', message: 'Run', sessionId: { $ne: null }, stream: false });
237
+ assert.equal(response.status, 400);
238
+ assert.equal(runtime.seen.length, 0);
239
+ });
240
+ });
241
+ });
242
+
243
+ test('a continued session runs under its own provider and project, as a chat send does', async () => {
244
+ await withIsolatedDatabase(async () => {
245
+ sessionsDb.createAppSession('app-codex', 'codex', '/home/test/project', 'Codex one');
246
+ const claudeCalls: unknown[] = [];
247
+ const codexCalls: unknown[] = [];
248
+ const dependencies = createDependencies(async (_command, options, writer) => {
249
+ claudeCalls.push(options);
250
+ writer.send(createNormalizedMessage({ kind: 'complete', provider: 'claude', sessionId: null, exitCode: 0 }));
251
+ });
252
+ dependencies.queryCodex = async (_command, options, writer) => {
253
+ codexCalls.push(options);
254
+ writer.send(createNormalizedMessage({ kind: 'complete', provider: 'codex', sessionId: null, exitCode: 0 }));
255
+ };
256
+ await withAgentServer(dependencies, async (baseUrl) => {
257
+ // No provider named: the row's, not the default.
258
+ const continued = await post(baseUrl, { projectPath: '/home/test/project', message: 'More', sessionId: 'app-codex', stream: false });
259
+ assert.equal(continued.status, 200);
260
+ assert.deepEqual([claudeCalls.length, codexCalls.length], [0, 1]);
261
+
262
+ // Another provider named: refused, not re-homed.
263
+ const mismatched = await post(baseUrl, { projectPath: '/home/test/project', message: 'More', sessionId: 'app-codex', provider: 'claude', stream: false });
264
+ assert.equal(mismatched.status, 400);
265
+ assert.equal(claudeCalls.length, 0);
266
+
267
+ // Another directory named: not that session.
268
+ const elsewhere = await post(baseUrl, { projectPath: '/home/test/other', message: 'More', sessionId: 'app-codex', stream: false });
269
+ assert.equal(elsewhere.status, 500);
270
+ assert.match((await elsewhere.json() as { error: string }).error, /belongs to project/);
271
+ assert.equal(codexCalls.length, 1);
272
+ });
273
+ });
274
+ });
275
+
276
+ test('a busy session is refused before anything is cloned', async () => {
277
+ await withIsolatedDatabase(async () => {
278
+ const runtime = createHeldRuntime();
279
+ const dependencies = createDependencies(runtime.queryClaude);
280
+ let clones = 0;
281
+ dependencies.spawnProcess = (() => { clones += 1; throw new Error('no clone expected'); }) as unknown as AgentDependencies['spawnProcess'];
282
+ await withAgentServer(dependencies, async (baseUrl) => {
283
+ const first = post(baseUrl, { projectPath: '/home/test/project', message: 'Run it' });
284
+ for (let i = 0; i < 50 && runtime.seen.length === 0; i += 1) {
285
+ await new Promise((resolve) => { setTimeout(resolve, 20); });
286
+ }
287
+ const [running] = chatRunRegistry.listRunningRuns();
288
+ assert.ok(running);
289
+
290
+ const retry = await post(baseUrl, { githubUrl: 'https://github.com/owner/repo.git', projectPath: '/home/test/project', message: 'Again', sessionId: running.sessionId, stream: false });
291
+ assert.equal(retry.status, 409);
292
+ assert.equal(clones, 0, 'the refusal comes before the clone step');
293
+
294
+ runtime.release();
295
+ await first;
296
+ });
297
+ });
298
+ });
299
+
300
+ test('a run a tab aborted ends without branching or opening a PR, and says so', async () => {
301
+ await withIsolatedDatabase(async () => {
302
+ const runtime = createHeldRuntime();
303
+ let gitCalls = 0;
304
+ const dependencies = createDependencies(async (command, options, writer) => {
305
+ await runtime.queryClaude(command, options, writer);
306
+ });
307
+ dependencies.spawnProcess = (() => { gitCalls += 1; throw new Error('git should not run after an abort'); }) as unknown as AgentDependencies['spawnProcess'];
308
+ await withAgentServer(dependencies, async (baseUrl) => {
309
+ const responsePromise = post(baseUrl, { projectPath: '/home/test/project', message: 'Run it', createBranch: true, stream: false });
310
+ for (let i = 0; i < 50 && runtime.seen.length === 0; i += 1) {
311
+ await new Promise((resolve) => { setTimeout(resolve, 20); });
312
+ }
313
+ const [running] = chatRunRegistry.listRunningRuns();
314
+ assert.ok(running);
315
+ // What chat.abort does: the run completes as aborted; the runtime then returns.
316
+ chatRunRegistry.completeRun(running.sessionId, { exitCode: 1, aborted: true });
317
+ runtime.release();
318
+
319
+ const response = await responsePromise;
320
+ const body = await response.json() as { success: boolean; aborted?: boolean; branch?: unknown };
321
+ assert.equal(body.success, false);
322
+ assert.equal(body.aborted, true);
323
+ assert.equal(body.branch, undefined);
324
+ assert.equal(gitCalls, 0);
325
+ });
326
+ });
327
+ });
328
+
329
+ test('a non-streaming run answers with the assistant\'s replies, its token usage and the provider id', async () => {
330
+ await withIsolatedDatabase(async () => {
331
+ const dependencies = createDependencies(async (_command, _options, writer) => {
332
+ writer.send(createNormalizedMessage({ kind: 'session_created', provider: 'claude', sessionId: 'native-7', newSessionId: 'native-7' }));
333
+ writer.send(createNormalizedMessage({ kind: 'text', provider: 'claude', sessionId: 'native-7', role: 'assistant', content: 'pong' }));
334
+ writer.send(createNormalizedMessage({
335
+ kind: 'status', text: 'token_budget', provider: 'claude', sessionId: 'native-7',
336
+ tokenBudget: { used: 130, total: 160_000, inputTokens: 100, outputTokens: 30, cacheReadTokens: 60, cacheCreationTokens: 10, cacheTokens: 70, breakdown: { input: 100, output: 30 } },
337
+ }));
338
+ writer.send(createNormalizedMessage({ kind: 'complete', provider: 'claude', sessionId: 'native-7', exitCode: 0 }));
339
+ });
340
+ await withAgentServer(dependencies, async (baseUrl) => {
341
+ const response = await post(baseUrl, { projectPath: '/home/test/project', message: 'ping', stream: false });
342
+ const body = await response.json() as { providerSessionId: string; messages: Array<{ content: string }>; tokens: Record<string, number> };
343
+ assert.equal(body.providerSessionId, 'native-7');
344
+ assert.deepEqual(body.messages.map((message) => message.content), ['pong']);
345
+ assert.deepEqual(body.tokens, { inputTokens: 100, outputTokens: 30, cacheReadTokens: 60, cacheCreationTokens: 10, totalTokens: 130 });
346
+ });
347
+ });
348
+ });
@@ -30,6 +30,23 @@ function createDependencies(
30
30
  githubTokens: { getActiveGithubToken: () => null },
31
31
  projects: { createProjectPath: () => ({ outcome: 'created' }) },
32
32
  models: {} as AgentDependencies['models'],
33
+ sessions: {
34
+ getSessionById: () => null,
35
+ getSessionByProviderSessionId: () => null,
36
+ createAppSession: () => ({ sessionId: 'app-session-1' }),
37
+ },
38
+ // A registry stand-in: the runtime writes straight to the audience, which
39
+ // is all the tests above this file's registration tests need.
40
+ runs: {
41
+ startRun: ((input: { connection: { send(data: string): void } | null }) => ({
42
+ // The gateway writer's shape: a runtime that feature-detects it (codex)
43
+ // hands it objects to serialize, as the real one expects.
44
+ writer: { isWebSocketWriter: true, send: (data: unknown) => input.connection?.send(JSON.stringify(data)), getSessionId: () => null },
45
+ events: [],
46
+ })) as unknown as AgentDependencies['runs']['startRun'],
47
+ completeRunIfCurrent: () => undefined,
48
+ isProcessing: () => false,
49
+ },
33
50
  queryClaude: unexpectedProviderCall as AgentDependencies['queryClaude'],
34
51
  queryCursor: unexpectedProviderCall as AgentDependencies['queryCursor'],
35
52
  queryCodex: unexpectedProviderCall as AgentDependencies['queryCodex'],
@@ -154,7 +171,7 @@ test('GitHub cloning keeps credentials out of arguments and remote URL', async (
154
171
  assert.equal(cloneEnvironment?.CLOUDCLI_GITHUB_TOKEN, token);
155
172
  assert.equal(cloneEnvironment?.GIT_CONFIG_KEY_0, 'credential.helper');
156
173
  assert.equal(cloneEnvironment?.GIT_CONFIG_VALUE_0, '');
157
- assert.equal(cloneEnvironment?.GIT_CONFIG_KEY_1, 'credential.helper');
174
+ assert.equal(cloneEnvironment?.GIT_CONFIG_KEY_1, 'credential.https://github.com.helper');
158
175
  });
159
176
 
160
177
  test('Agent route reuses a matching checkout without cloning or deleting it', async () => {
@@ -457,6 +457,19 @@ const addSessionEffortColumn = (db: Database): void => {
457
457
  addColumnToTableIfNotExists(db, 'sessions', columnNames, 'effort', 'TEXT');
458
458
  };
459
459
 
460
+ /**
461
+ * Adds the reasoning-effort metadata columns to the custom-model library.
462
+ *
463
+ * Existing rows stay NULL on purpose: no effort levels were ever declared for
464
+ * them, and a guessed list could offer levels the model rejects. Those models
465
+ * keep hiding the composer's Reasoning section until the user declares levels.
466
+ */
467
+ const addProviderModelEffortColumns = (db: Database): void => {
468
+ const columnNames = getTableInfo(db, 'provider_models').map((column) => column.name);
469
+ addColumnToTableIfNotExists(db, 'provider_models', columnNames, 'effort_values', 'TEXT DEFAULT NULL');
470
+ addColumnToTableIfNotExists(db, 'provider_models', columnNames, 'effort_default', 'TEXT DEFAULT NULL');
471
+ };
472
+
460
473
  const ensureProjectsForSessionPaths = (db: Database): void => {
461
474
  if (!tableExists(db, 'sessions')) {
462
475
  return;
@@ -504,6 +517,7 @@ export const runMigrations = (db: Database) => {
504
517
  CREATE INDEX IF NOT EXISTS idx_provider_models_provider_order
505
518
  ON provider_models(provider, sort_order, id)
506
519
  `);
520
+ addProviderModelEffortColumns(db);
507
521
  db.exec(USER_PREFERENCES_TABLE_SCHEMA_SQL);
508
522
  db.exec(SESSION_DRAFTS_TABLE_SCHEMA_SQL);
509
523
  db.exec(SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL);
@@ -1,5 +1,6 @@
1
1
  import { getConnection } from '@/modules/database/connection.js';
2
2
  import type {
3
+ CustomProviderModelEffort,
3
4
  CustomProviderModelInput,
4
5
  CustomProviderModelRecord,
5
6
  LLMProvider,
@@ -11,6 +12,56 @@ type CustomProviderModelRow = {
11
12
  model_id: string;
12
13
  model_name: string;
13
14
  sort_order: number;
15
+ effort_values: string | null;
16
+ effort_default: string | null;
17
+ };
18
+
19
+ const CUSTOM_PROVIDER_MODEL_COLUMNS = `
20
+ id, provider, model_id, model_name, sort_order, effort_values, effort_default
21
+ `;
22
+
23
+ /**
24
+ * Decodes the stored effort columns. Anything that is not a non-empty JSON
25
+ * array of strings reads back as "no metadata" rather than failing the whole
26
+ * catalog, so one hand-edited row cannot break every model picker.
27
+ */
28
+ const readStoredEffort = (row: CustomProviderModelRow): CustomProviderModelEffort | null => {
29
+ if (!row.effort_values) {
30
+ return null;
31
+ }
32
+
33
+ let parsed: unknown;
34
+ try {
35
+ parsed = JSON.parse(row.effort_values);
36
+ } catch {
37
+ return null;
38
+ }
39
+
40
+ if (!Array.isArray(parsed)) {
41
+ return null;
42
+ }
43
+ const values = parsed.filter((value): value is string => typeof value === 'string' && value.length > 0);
44
+ if (values.length === 0) {
45
+ return null;
46
+ }
47
+
48
+ return row.effort_default && values.includes(row.effort_default)
49
+ ? { values, default: row.effort_default }
50
+ : { values };
51
+ };
52
+
53
+ /** Encodes effort metadata into the two nullable columns; no levels means NULL for both. */
54
+ const toStoredEffortColumns = (
55
+ effort: CustomProviderModelEffort | null | undefined,
56
+ ): { values: string | null; defaultValue: string | null } => {
57
+ if (!effort || effort.values.length === 0) {
58
+ return { values: null, defaultValue: null };
59
+ }
60
+
61
+ return {
62
+ values: JSON.stringify(effort.values),
63
+ defaultValue: effort.default ?? null,
64
+ };
14
65
  };
15
66
 
16
67
  const toCustomProviderModelRecord = (
@@ -21,6 +72,7 @@ const toCustomProviderModelRecord = (
21
72
  modelId: row.model_id,
22
73
  model: row.model_name,
23
74
  sortOrder: row.sort_order,
75
+ effort: readStoredEffort(row),
24
76
  });
25
77
 
26
78
  const readCustomProviderModelRow = (
@@ -28,7 +80,7 @@ const readCustomProviderModelRow = (
28
80
  recordId: number,
29
81
  ): CustomProviderModelRow | null => {
30
82
  const row = getConnection().prepare(`
31
- SELECT id, provider, model_id, model_name, sort_order
83
+ SELECT ${CUSTOM_PROVIDER_MODEL_COLUMNS}
32
84
  FROM provider_models
33
85
  WHERE provider = ? AND id = ?
34
86
  `).get(provider, recordId) as CustomProviderModelRow | undefined;
@@ -47,7 +99,7 @@ const readCustomProviderModelRow = (
47
99
  export const providerModelsDb = {
48
100
  listCustomProviderModels(provider: LLMProvider): CustomProviderModelRecord[] {
49
101
  const rows = getConnection().prepare(`
50
- SELECT id, provider, model_id, model_name, sort_order
102
+ SELECT ${CUSTOM_PROVIDER_MODEL_COLUMNS}
51
103
  FROM provider_models
52
104
  WHERE provider = ?
53
105
  ORDER BY sort_order ASC, lower(model_name) ASC, id ASC
@@ -69,7 +121,7 @@ export const providerModelsDb = {
69
121
  modelId: string,
70
122
  ): CustomProviderModelRecord | null {
71
123
  const row = getConnection().prepare(`
72
- SELECT id, provider, model_id, model_name, sort_order
124
+ SELECT ${CUSTOM_PROVIDER_MODEL_COLUMNS}
73
125
  FROM provider_models
74
126
  WHERE provider = ? AND model_id = ?
75
127
  `).get(provider, modelId) as CustomProviderModelRow | undefined;
@@ -88,10 +140,20 @@ export const providerModelsDb = {
88
140
  WHERE provider = ?
89
141
  `).get(provider) as { next_order: number };
90
142
 
143
+ const effort = toStoredEffortColumns(input.effort);
91
144
  const result = db.prepare(`
92
- INSERT INTO provider_models (provider, model_id, model_name, sort_order)
93
- VALUES (?, ?, ?, ?)
94
- `).run(provider, input.id, input.model, nextOrder.next_order);
145
+ INSERT INTO provider_models (
146
+ provider, model_id, model_name, sort_order, effort_values, effort_default
147
+ )
148
+ VALUES (?, ?, ?, ?, ?, ?)
149
+ `).run(
150
+ provider,
151
+ input.id,
152
+ input.model,
153
+ nextOrder.next_order,
154
+ effort.values,
155
+ effort.defaultValue,
156
+ );
95
157
 
96
158
  const row = readCustomProviderModelRow(provider, Number(result.lastInsertRowid));
97
159
  if (!row) {
@@ -119,6 +181,17 @@ export const providerModelsDb = {
119
181
  WHERE provider = ? AND id = ?
120
182
  `).run(input.id, input.model, provider, recordId);
121
183
 
184
+ // An omitted `effort` keeps the stored levels so clients that only edit
185
+ // the name or id cannot wipe them; `null` explicitly clears them.
186
+ if (input.effort !== undefined) {
187
+ const effort = toStoredEffortColumns(input.effort);
188
+ db.prepare(`
189
+ UPDATE provider_models
190
+ SET effort_values = ?, effort_default = ?
191
+ WHERE provider = ? AND id = ?
192
+ `).run(effort.values, effort.defaultValue, provider, recordId);
193
+ }
194
+
122
195
  if (previous.model_id !== input.id) {
123
196
  db.prepare(`
124
197
  UPDATE sessions
@@ -695,6 +695,14 @@ export const sessionsDb = {
695
695
  return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
696
696
  },
697
697
 
698
+ /** Used by the OpenCode synchronizer to remove indexed child sessions by their native id. */
699
+ deleteSessionByProviderSessionId(providerSessionId: string, provider: string): boolean {
700
+ const db = getConnection();
701
+ return db
702
+ .prepare('DELETE FROM sessions WHERE provider_session_id = ? AND provider = ?')
703
+ .run(providerSessionId, provider).changes > 0;
704
+ },
705
+
698
706
  /**
699
707
  * Lists every indexed session that claims a transcript file on disk.
700
708
  *
@@ -174,6 +174,11 @@ CREATE TABLE IF NOT EXISTS app_config (
174
174
  * controlled in each provider's `-models.provider.ts` adapter so they can be
175
175
  * updated without migrating application data. `model_id` is unique only within
176
176
  * a provider because different CLIs can accept the same identifier.
177
+ *
178
+ * `effort_values` (JSON array of level ids) and `effort_default` hold the
179
+ * reasoning-effort levels the user declared for the model; both stay NULL when
180
+ * none were declared. They sit after the timestamps so fresh installs match the
181
+ * column order that `runMigrations` produces on upgraded databases.
177
182
  */
178
183
  export const PROVIDER_MODELS_TABLE_SCHEMA_SQL = `
179
184
  CREATE TABLE IF NOT EXISTS provider_models (
@@ -184,6 +189,8 @@ CREATE TABLE IF NOT EXISTS provider_models (
184
189
  sort_order INTEGER NOT NULL DEFAULT 0,
185
190
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
186
191
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
192
+ effort_values TEXT DEFAULT NULL,
193
+ effort_default TEXT DEFAULT NULL,
187
194
  UNIQUE(provider, model_id)
188
195
  );
189
196
  `;