@cloudcli-ai/cloudcli 1.37.2 → 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 (453) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.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-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.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-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +11 -4
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.module.js +9 -2
  65. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  66. package/dist-server/server/modules/agent/agent.routes.js +177 -130
  67. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  68. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  69. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  70. package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
  71. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  73. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  74. package/dist-server/server/modules/database/connection.js +1 -1
  75. package/dist-server/server/modules/database/connection.js.map +1 -1
  76. package/dist-server/server/modules/database/index.js +5 -0
  77. package/dist-server/server/modules/database/index.js.map +1 -1
  78. package/dist-server/server/modules/database/migrations.js +45 -1
  79. package/dist-server/server/modules/database/migrations.js.map +1 -1
  80. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  81. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  83. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  84. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  85. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  86. package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
  87. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  88. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  89. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  90. package/dist-server/server/modules/database/schema.js +99 -0
  91. package/dist-server/server/modules/database/schema.js.map +1 -1
  92. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  93. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  94. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  96. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  97. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  98. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  99. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  100. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  101. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  102. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  103. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  104. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  105. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  106. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  107. package/dist-server/server/modules/git/git.routes.js +92 -5
  108. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  109. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  110. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  111. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  112. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  113. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  114. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  115. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  116. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  117. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  118. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  119. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  120. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  121. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  122. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  123. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  124. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  125. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  126. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  127. package/dist-server/server/modules/providers/index.js +3 -0
  128. package/dist-server/server/modules/providers/index.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
  130. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  132. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  133. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
  134. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  135. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
  136. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  137. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  138. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
  140. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  142. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  143. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  144. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  146. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  148. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
  152. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  155. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
  156. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  157. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  158. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  160. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
  162. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  164. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
  165. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  166. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  167. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  168. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  169. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/provider.routes.js +134 -2
  171. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  172. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  173. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  174. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  175. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  176. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
  177. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  179. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  181. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  182. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  183. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  185. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
  187. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  188. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  189. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  190. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  191. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  192. package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
  193. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
  195. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  196. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  197. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  199. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  201. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  203. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  205. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  206. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
  207. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  209. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  210. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  211. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  212. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  213. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  215. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  217. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
  219. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  221. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  222. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
  223. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  224. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  225. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  226. package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
  227. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  228. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  229. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  230. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  231. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  232. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  233. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  234. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  235. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  236. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  237. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  239. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  240. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
  241. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  242. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  243. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  244. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  245. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  246. package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
  247. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  248. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  249. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  250. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  251. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  252. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  253. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  254. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
  255. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  256. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  257. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  258. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
  259. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  260. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  261. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  262. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  263. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  264. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  265. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  266. package/dist-server/server/modules/user/user.module.js +10 -1
  267. package/dist-server/server/modules/user/user.module.js.map +1 -1
  268. package/dist-server/server/modules/user/user.routes.js +44 -0
  269. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  270. package/dist-server/server/modules/user/user.service.js +75 -0
  271. package/dist-server/server/modules/user/user.service.js.map +1 -1
  272. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  273. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  274. package/dist-server/server/modules/websocket/index.js +7 -0
  275. package/dist-server/server/modules/websocket/index.js.map +1 -1
  276. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
  277. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  278. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  279. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  280. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
  281. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  282. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  283. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  284. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
  285. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  286. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  287. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  288. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  289. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  290. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  291. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  292. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  293. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  294. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  295. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  296. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
  297. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  298. package/dist-server/server/shared/claude-cli-path.js +22 -5
  299. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  300. package/dist-server/server/shared/index.js +3 -0
  301. package/dist-server/server/shared/index.js.map +1 -0
  302. package/dist-server/server/shared/message-unification.js +376 -0
  303. package/dist-server/server/shared/message-unification.js.map +1 -0
  304. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  305. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  306. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  307. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  308. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  309. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  310. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  311. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  312. package/dist-server/server/shared/utils.js +160 -0
  313. package/dist-server/server/shared/utils.js.map +1 -1
  314. package/electron/desktopWindow.js +6 -0
  315. package/electron/viewHost.js +25 -2
  316. package/package.json +20 -17
  317. package/server/index.ts +16 -5
  318. package/server/modules/agent/agent.module.ts +11 -1
  319. package/server/modules/agent/agent.routes.ts +200 -142
  320. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  321. package/server/modules/agent/tests/agent.routes.test.ts +51 -2
  322. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  323. package/server/modules/database/connection.ts +2 -1
  324. package/server/modules/database/index.ts +10 -0
  325. package/server/modules/database/migrations.ts +51 -0
  326. package/server/modules/database/repositories/provider-models.ts +79 -6
  327. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  328. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  329. package/server/modules/database/repositories/sessions.db.ts +204 -3
  330. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  331. package/server/modules/database/schema.ts +103 -0
  332. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  333. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  334. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  335. package/server/modules/file-tree/file-tree.module.ts +2 -1
  336. package/server/modules/file-tree/file-tree.service.ts +49 -7
  337. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  338. package/server/modules/git/git-branch-diff.service.ts +204 -0
  339. package/server/modules/git/git-branch.service.ts +2 -12
  340. package/server/modules/git/git.routes.ts +100 -6
  341. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  342. package/server/modules/projects/project-clone.routes.ts +142 -0
  343. package/server/modules/projects/projects.routes.ts +16 -81
  344. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  345. package/server/modules/projects/services/project-clone.service.ts +102 -34
  346. package/server/modules/projects/services/project-delete.service.ts +13 -1
  347. package/server/modules/projects/services/project-star.service.ts +2 -2
  348. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  349. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  350. package/server/modules/providers/index.ts +4 -0
  351. package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
  352. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  353. package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
  354. package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
  355. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  356. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
  357. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  358. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  359. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  360. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  361. package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
  362. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
  363. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  364. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
  365. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  366. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  367. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
  368. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  369. package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
  370. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  371. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  372. package/server/modules/providers/provider.routes.ts +162 -2
  373. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  374. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  375. package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
  376. package/server/modules/providers/services/provider-models.service.ts +71 -4
  377. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  378. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  379. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  380. package/server/modules/providers/services/session-history-cache.service.ts +150 -0
  381. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  382. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  383. package/server/modules/providers/services/sessions.service.ts +309 -17
  384. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  385. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  386. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  387. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  388. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  389. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  390. package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
  391. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  392. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  393. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  394. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  395. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  396. package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
  397. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  398. package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
  399. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  400. package/server/modules/providers/tests/opencode-models.test.ts +266 -8
  401. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  402. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  403. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  404. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  405. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  406. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  407. package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
  408. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  409. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  410. package/server/modules/providers/tests/sessions.service.test.ts +303 -1
  411. package/server/modules/providers/tests/skills.test.ts +21 -5
  412. package/server/modules/scheduled-messages/index.ts +8 -0
  413. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  414. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
  415. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  416. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
  417. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  418. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  419. package/server/modules/user/tests/user.service.test.ts +90 -0
  420. package/server/modules/user/user.module.ts +10 -1
  421. package/server/modules/user/user.routes.ts +44 -0
  422. package/server/modules/user/user.service.ts +105 -0
  423. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  424. package/server/modules/websocket/README.md +4 -2
  425. package/server/modules/websocket/index.ts +8 -0
  426. package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
  427. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  428. package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
  429. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  430. package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
  431. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  432. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  433. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  434. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  435. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  436. package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
  437. package/server/shared/claude-cli-path.ts +24 -6
  438. package/server/shared/index.ts +3 -0
  439. package/server/shared/interfaces.ts +91 -0
  440. package/server/shared/message-unification.ts +449 -0
  441. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  442. package/server/shared/tests/message-unification.test.ts +218 -0
  443. package/server/shared/tests/read-only-roots.test.ts +139 -0
  444. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  445. package/server/shared/types.ts +382 -7
  446. package/server/shared/utils.ts +176 -0
  447. package/dist/assets/channel-_falYbU1.js +0 -1
  448. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  449. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  450. package/dist/assets/index-CzPLBvXT.css +0 -32
  451. package/dist/assets/index-DFkBBuSA.js +0 -1549
  452. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  453. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -1,11 +1,15 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
7
  import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '@/modules/database/index.js';
8
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
8
9
  import { sessionsService } from '@/modules/providers/services/sessions.service.js';
10
+ import { chatRunRegistry } from '@/modules/websocket/index.js';
11
+ import type { IProvider } from '@/shared/interfaces.js';
12
+ import type { BackgroundTaskSummary } from '@/shared/types.js';
9
13
 
10
14
  async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
11
15
  const previousDatabasePath = process.env.DATABASE_PATH;
@@ -125,3 +129,301 @@ test('recent sessions map project metadata and preserve database pagination', {
125
129
  });
126
130
  });
127
131
  });
132
+
133
+ /** One Claude transcript row of user or assistant text, linked by uuid chain. */
134
+ function claudeTextRow(
135
+ sessionId: string,
136
+ role: 'user' | 'assistant',
137
+ text: string,
138
+ ordinal: number,
139
+ ): Record<string, unknown> {
140
+ return {
141
+ type: role,
142
+ uuid: `row-${ordinal}`,
143
+ parentUuid: ordinal === 0 ? null : `row-${ordinal - 1}`,
144
+ timestamp: new Date(Date.UTC(2026, 0, 1, 0, 0, ordinal)).toISOString(),
145
+ sessionId,
146
+ message: { role, content: [{ type: 'text', text }] },
147
+ };
148
+ }
149
+
150
+ test('history pages are sliced from the cached full transcript and see appended rows', { concurrency: false }, async () => {
151
+ const transcriptDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-history-'));
152
+ const sessionId = 'claude-history-cache-session';
153
+ const transcriptPath = path.join(transcriptDirectory, `${sessionId}.jsonl`);
154
+
155
+ try {
156
+ await withIsolatedDatabase(async () => {
157
+ const rows = [
158
+ claudeTextRow(sessionId, 'user', 'one', 0),
159
+ claudeTextRow(sessionId, 'assistant', 'reply one', 1),
160
+ claudeTextRow(sessionId, 'user', 'two', 2),
161
+ claudeTextRow(sessionId, 'assistant', 'reply two', 3),
162
+ ];
163
+ await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
164
+ sessionsDb.createSession(
165
+ sessionId,
166
+ 'claude',
167
+ '/tmp/history-cache-project',
168
+ 'History cache conversation',
169
+ '2026-01-01T00:00:00.000Z',
170
+ '2026-01-01T00:00:10.000Z',
171
+ transcriptPath,
172
+ );
173
+
174
+ const page = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
175
+ assert.equal(page.total, 4);
176
+ assert.equal(page.hasMore, true);
177
+ assert.deepEqual(page.messages.map((message) => message.content), ['two', 'reply two']);
178
+
179
+ // A row appended after the page was cached must appear on the next read.
180
+ await appendFile(
181
+ transcriptPath,
182
+ `${JSON.stringify(claudeTextRow(sessionId, 'user', 'three', 4))}\n`,
183
+ 'utf8',
184
+ );
185
+ const refreshed = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
186
+ assert.equal(refreshed.total, 5);
187
+ assert.deepEqual(refreshed.messages.map((message) => message.content), ['reply two', 'three']);
188
+
189
+ // An older page keeps the tail-offset contract while served from cache.
190
+ const older = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 2 });
191
+ assert.deepEqual(older.messages.map((message) => message.content), ['reply one', 'two']);
192
+ assert.equal(older.hasMore, true);
193
+ });
194
+ } finally {
195
+ await rm(transcriptDirectory, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ /**
200
+ * Stands in for the provider registry with one provider per id, each with the
201
+ * runtime given — the seam through which a runtime's background work reaches
202
+ * the running-sessions list without an SDK run behind it.
203
+ */
204
+ async function withProviders(
205
+ runtimes: Partial<Record<'claude' | 'codex', IProvider['runtime']>>,
206
+ runTest: () => void | Promise<void>,
207
+ ): Promise<void> {
208
+ const realListProviders = providerRegistry.listProviders;
209
+ providerRegistry.listProviders = () =>
210
+ Object.entries(runtimes).map(([id, runtime]) => ({ id, runtime }) as IProvider);
211
+ try {
212
+ await runTest();
213
+ } finally {
214
+ providerRegistry.listProviders = realListProviders;
215
+ chatRunRegistry.clearAll();
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Runs `runTest` against a Claude CLI process registry containing exactly
221
+ * `records`, so the running-sessions tests never read the developer's real
222
+ * `~/.claude/sessions`.
223
+ */
224
+ async function withClaudeCliRegistry(
225
+ records: Array<Record<string, unknown>>,
226
+ runTest: () => void | Promise<void>,
227
+ ): Promise<void> {
228
+ const homeDirectory = await mkdtemp(path.join(os.tmpdir(), 'claude-cli-registry-'));
229
+ await mkdir(path.join(homeDirectory, '.claude', 'sessions'), { recursive: true });
230
+ for (const record of records) {
231
+ await writeFile(
232
+ path.join(homeDirectory, '.claude', 'sessions', `${record.pid}.json`),
233
+ JSON.stringify(record),
234
+ 'utf8',
235
+ );
236
+ }
237
+
238
+ const realHomedir = os.homedir;
239
+ (os as unknown as { homedir: () => string }).homedir = () => homeDirectory;
240
+ try {
241
+ await runTest();
242
+ } finally {
243
+ (os as unknown as { homedir: () => string }).homedir = realHomedir;
244
+ await rm(homeDirectory, { recursive: true, force: true });
245
+ }
246
+ }
247
+
248
+ const task = (taskId: string, startedAt: number): BackgroundTaskSummary => ({
249
+ taskId,
250
+ toolUseId: `toolu_${taskId}`,
251
+ taskType: 'local_agent',
252
+ description: `Task ${taskId}`,
253
+ startedAt,
254
+ });
255
+
256
+ test('running sessions list a session whose background work outlived its turn', { concurrency: false }, async () => {
257
+ await withIsolatedDatabase(async () => {
258
+ sessionsDb.createAppSession('held-session', 'claude', '/tmp/running-project');
259
+ // The turn ran and completed: the registry still holds the finished run.
260
+ const run = chatRunRegistry.startRun({
261
+ appSessionId: 'held-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
262
+ });
263
+ assert.ok(run);
264
+ run.writer.send({ kind: 'text', content: 'launched', sessionId: 'held-session', provider: 'claude' });
265
+ run.writer.sendComplete({ exitCode: 0 });
266
+ assert.equal(run.status, 'completed');
267
+
268
+ const tasks = [task('late', 2_000), task('early', 1_000)];
269
+ await withClaudeCliRegistry([], async () => await withProviders(
270
+ { claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] } },
271
+ async () => {
272
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
273
+ sessionId: 'held-session',
274
+ provider: 'claude',
275
+ startedAt: 1_000,
276
+ lastSeq: run.lastSeq,
277
+ background: true,
278
+ canInterrupt: false,
279
+ tasks,
280
+ }]);
281
+ assert.ok(run.lastSeq > 0, 'the finished run still reports its sequence for replay');
282
+ },
283
+ ));
284
+ });
285
+ });
286
+
287
+ test('running sessions carry their tasks on a chat run that is still going', { concurrency: false }, async () => {
288
+ await withIsolatedDatabase(async () => {
289
+ sessionsDb.createAppSession('busy-session', 'claude', '/tmp/running-project');
290
+ sessionsDb.createAppSession('idle-session', 'codex', '/tmp/running-project');
291
+ const run = chatRunRegistry.startRun({
292
+ appSessionId: 'busy-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
293
+ });
294
+ assert.ok(run);
295
+
296
+ const tasks = [task('agent', 5_000)];
297
+ await withClaudeCliRegistry([], async () => await withProviders(
298
+ {
299
+ claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'busy-session', tasks }] },
300
+ // A runtime without background work contributes chat runs alone.
301
+ codex: { run: async () => undefined, abort: () => false },
302
+ },
303
+ async () => {
304
+ const sessions = await sessionsService.listRunningSessions();
305
+ assert.equal(sessions.length, 1, 'a session with a running chat run is listed once');
306
+ assert.deepEqual(sessions[0], {
307
+ sessionId: 'busy-session',
308
+ provider: 'claude',
309
+ startedAt: run.startedAt,
310
+ lastSeq: 0,
311
+ tasks,
312
+ });
313
+ },
314
+ ));
315
+ });
316
+ });
317
+
318
+ test('running sessions with no background work are the chat runs alone', { concurrency: false }, async () => {
319
+ await withIsolatedDatabase(async () => {
320
+ sessionsDb.createAppSession('plain-session', 'codex', '/tmp/running-project');
321
+ const run = chatRunRegistry.startRun({
322
+ appSessionId: 'plain-session', provider: 'codex', providerSessionId: null, connection: null, userId: null,
323
+ });
324
+ assert.ok(run);
325
+
326
+ await withClaudeCliRegistry([], async () => await withProviders(
327
+ { codex: { run: async () => undefined, abort: () => false } },
328
+ async () => {
329
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
330
+ sessionId: 'plain-session', provider: 'codex', startedAt: run.startedAt, lastSeq: 0,
331
+ }]);
332
+ },
333
+ ));
334
+ });
335
+ });
336
+
337
+ test('running sessions report a turn driven by the Claude CLI', { concurrency: false }, async () => {
338
+ await withIsolatedDatabase(async () => {
339
+ sessionsDb.createAppSession('cli-session', 'claude', '/tmp/running-project');
340
+ sessionsDb.assignProviderSessionId('cli-session', 'claude-native-1');
341
+
342
+ // `process.pid` is the one pid guaranteed to be alive; omitting procStart
343
+ // keeps the fixture portable, since the start-time cross-check is Linux-only.
344
+ await withClaudeCliRegistry(
345
+ [{ pid: process.pid, sessionId: 'claude-native-1', status: 'busy', entrypoint: 'cli', startedAt: 4_000 }],
346
+ async () => await withProviders(
347
+ { claude: { run: async () => undefined, abort: () => false } },
348
+ async () => {
349
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
350
+ sessionId: 'cli-session',
351
+ provider: 'claude',
352
+ startedAt: 4_000,
353
+ lastSeq: 0,
354
+ canInterrupt: false,
355
+ statusText: 'Running in the Claude CLI',
356
+ }]);
357
+ },
358
+ ),
359
+ );
360
+ });
361
+ });
362
+
363
+ test('running sessions ignore registry files a crash left behind', { concurrency: false }, async () => {
364
+ await withIsolatedDatabase(async () => {
365
+ sessionsDb.createAppSession('stale-session', 'claude', '/tmp/running-project');
366
+ sessionsDb.assignProviderSessionId('stale-session', 'claude-native-stale');
367
+
368
+ await withClaudeCliRegistry(
369
+ [
370
+ // The process is gone: signal 0 cannot find this pid.
371
+ { pid: 4_194_302, sessionId: 'claude-native-stale', status: 'busy', startedAt: 1_000 },
372
+ // Alive, but waiting for input rather than producing a response.
373
+ { pid: process.pid, sessionId: 'claude-native-idle', status: 'idle', startedAt: 2_000 },
374
+ ],
375
+ async () => await withProviders(
376
+ { claude: { run: async () => undefined, abort: () => false } },
377
+ async () => {
378
+ assert.deepEqual(await sessionsService.listRunningSessions(), []);
379
+ },
380
+ ),
381
+ );
382
+ });
383
+ });
384
+
385
+ test('a CLI turn does not duplicate a session already running a chat run', { concurrency: false }, async () => {
386
+ await withIsolatedDatabase(async () => {
387
+ sessionsDb.createAppSession('both-session', 'claude', '/tmp/running-project');
388
+ sessionsDb.assignProviderSessionId('both-session', 'claude-native-both');
389
+ const run = chatRunRegistry.startRun({
390
+ appSessionId: 'both-session', provider: 'claude', providerSessionId: 'claude-native-both', connection: null, userId: null,
391
+ });
392
+ assert.ok(run);
393
+
394
+ await withClaudeCliRegistry(
395
+ [{ pid: process.pid, sessionId: 'claude-native-both', status: 'busy', startedAt: 3_000 }],
396
+ async () => await withProviders(
397
+ { claude: { run: async () => undefined, abort: () => false } },
398
+ async () => {
399
+ const sessions = await sessionsService.listRunningSessions();
400
+ assert.equal(sessions.length, 1, 'the chat run already covers this session');
401
+ assert.equal(sessions[0].canInterrupt, undefined, 'the interruptible chat run entry wins');
402
+ },
403
+ ),
404
+ );
405
+ });
406
+ });
407
+
408
+ test('an unreadable registry never fails the running-sessions poll', { concurrency: false }, async () => {
409
+ await withIsolatedDatabase(async () => {
410
+ sessionsDb.createAppSession('safe-session', 'claude', '/tmp/running-project');
411
+ sessionsDb.assignProviderSessionId('safe-session', 'claude-native-safe');
412
+
413
+ await withClaudeCliRegistry(
414
+ [{ pid: process.pid, sessionId: 'claude-native-safe', status: 'busy', startedAt: 5_000 }],
415
+ async () => {
416
+ // A half-written file sits alongside the good one; it must be skipped
417
+ // rather than abort the whole poll.
418
+ await writeFile(path.join(os.homedir(), '.claude', 'sessions', 'broken.json'), '{"pid":', 'utf8');
419
+ await withProviders(
420
+ { claude: { run: async () => undefined, abort: () => false } },
421
+ async () => {
422
+ const sessions = await sessionsService.listRunningSessions();
423
+ assert.deepEqual(sessions.map((session) => session.sessionId), ['safe-session']);
424
+ },
425
+ );
426
+ },
427
+ );
428
+ });
429
+ });
@@ -69,7 +69,8 @@ const writeClaudePluginCommand = async (
69
69
 
70
70
  /**
71
71
  * This test covers Claude user/project skill folders plus plugin discovery from
72
- * installed plugin command files and fallback plugin skill files.
72
+ * installed plugin command files and plugin skill files, including a plugin that
73
+ * ships both.
73
74
  */
74
75
  test('providerSkillsService lists claude user, project, and enabled plugin skills', { concurrency: false }, async () => {
75
76
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
@@ -144,11 +145,19 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
144
145
  );
145
146
  await writeSkill(
146
147
  path.join(commandPluginInstallPath, 'skills'),
147
- 'ignored-command-plugin-skill-dir',
148
- 'ignored-command-plugin-skill',
149
- 'Command plugin fallback skill should be ignored',
148
+ 'command-plugin-skill-dir',
149
+ 'command-plugin-skill',
150
+ 'Skill shipped beside plugin commands',
150
151
  );
151
152
  await writeClaudePluginManifest(skillPluginInstallPath, 'ExampleSkills');
153
+ // A commands folder holding nothing this reader takes must not hide the
154
+ // skills beside it: plugins do ship commands in other agents' formats.
155
+ await fs.mkdir(path.join(skillPluginInstallPath, 'commands'), { recursive: true });
156
+ await fs.writeFile(
157
+ path.join(skillPluginInstallPath, 'commands', 'other-agent.toml'),
158
+ 'description = "Command for another agent"\n',
159
+ 'utf8',
160
+ );
152
161
  await writeSkill(
153
162
  path.join(skillPluginInstallPath, 'skills'),
154
163
  'claude-plugin-dir',
@@ -280,7 +289,14 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
280
289
  assert.equal(pluginCommand?.command, '/Notion:insert-row');
281
290
  assert.equal(pluginCommand?.description, 'Insert a Notion database row');
282
291
  assert.match(pluginCommand?.sourcePath ?? '', /commands[\\/]insert-row\.md$/);
283
- assert.equal(byName.has('ignored-command-plugin-skill'), false);
292
+ // A plugin that ships commands keeps its skills: the CLI offers both, so
293
+ // the menu lists both rather than letting the commands folder hide them.
294
+ const commandPluginSkill = byName.get('command-plugin-skill');
295
+ assert.equal(commandPluginSkill?.scope, 'plugin');
296
+ assert.equal(commandPluginSkill?.pluginName, 'Notion');
297
+ assert.equal(commandPluginSkill?.pluginId, 'notion@notion-marketplace');
298
+ assert.equal(commandPluginSkill?.command, '/Notion:command-plugin-skill');
299
+ assert.equal(commandPluginSkill?.description, 'Skill shipped beside plugin commands');
284
300
 
285
301
  const pluginSkill = byName.get('claude-plugin');
286
302
  assert.equal(pluginSkill?.scope, 'plugin');
@@ -0,0 +1,8 @@
1
+ // The HTTP surface for scheduling a message to a session, mounted by the app.
2
+ export { default as scheduledMessagesRoutes } from './scheduled-messages.routes.js';
3
+
4
+ // The timer that sends them, started and stopped with the server.
5
+ export {
6
+ initializeScheduledMessageDispatcher,
7
+ closeScheduledMessageDispatcher,
8
+ } from './services/scheduled-message-dispatcher.service.js';
@@ -0,0 +1,68 @@
1
+ import express from 'express';
2
+ import type { Request, Response } from 'express';
3
+
4
+ import { scheduledMessagesService } from '@/modules/scheduled-messages/services/scheduled-messages.service.js';
5
+ import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
6
+
7
+ type AuthenticatedRequest = Request & { user?: { id?: number | string } };
8
+
9
+ function readUserId(request: Request): number {
10
+ const userId = Number((request as AuthenticatedRequest).user?.id);
11
+ if (!Number.isInteger(userId)) {
12
+ throw new AppError('Authenticated user is required.', {
13
+ code: 'USER_REQUIRED',
14
+ statusCode: 401,
15
+ });
16
+ }
17
+ return userId;
18
+ }
19
+
20
+ function readString(value: unknown, field: string): string {
21
+ if (typeof value !== 'string' || !value.trim()) {
22
+ throw new AppError(`${field} is required.`, {
23
+ code: 'INVALID_REQUEST_BODY',
24
+ statusCode: 400,
25
+ });
26
+ }
27
+ return value;
28
+ }
29
+
30
+ const router = express.Router();
31
+
32
+ router.get(
33
+ '/',
34
+ asyncHandler(async (req: Request, res: Response) => {
35
+ const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : '';
36
+ const userId = readUserId(req);
37
+ res.json(createApiSuccessResponse(
38
+ sessionId
39
+ ? scheduledMessagesService.listForSession(userId, sessionId)
40
+ : scheduledMessagesService.listPending(userId),
41
+ ));
42
+ }),
43
+ );
44
+
45
+ router.post(
46
+ '/',
47
+ asyncHandler(async (req: Request, res: Response) => {
48
+ const body = (req.body ?? {}) as Record<string, unknown>;
49
+ const result = scheduledMessagesService.schedule({
50
+ userId: readUserId(req),
51
+ sessionId: readString(body.sessionId, 'sessionId'),
52
+ content: readString(body.content, 'content'),
53
+ options: body.options,
54
+ scheduledFor: readString(body.scheduledFor, 'scheduledFor'),
55
+ });
56
+ res.status(201).json(createApiSuccessResponse(result));
57
+ }),
58
+ );
59
+
60
+ router.delete(
61
+ '/:id',
62
+ asyncHandler(async (req: Request, res: Response) => {
63
+ scheduledMessagesService.cancel(readUserId(req), readString(req.params.id, 'id'));
64
+ res.json(createApiSuccessResponse({ cancelled: true }));
65
+ }),
66
+ );
67
+
68
+ export default router;
@@ -0,0 +1,216 @@
1
+ import { scheduledMessagesDb, sessionDraftsDb } from '@/modules/database/index.js';
2
+ import type { QueuedSessionMessageRecord, ScheduledMessageRow } from '@/modules/database/index.js';
3
+ import { chatRunRegistry, runDetachedChatTurn } from '@/modules/websocket/index.js';
4
+ import type { ProviderRuntimeGateway } from '@/modules/websocket/index.js';
5
+
6
+ /**
7
+ * How often due messages are looked for.
8
+ *
9
+ * A minute is the granularity the composer offers, and a claim is indexed on
10
+ * `(status, scheduled_for)`, so the poll is one cheap query. Anything finer
11
+ * would buy precision nobody asked for.
12
+ */
13
+ const POLL_INTERVAL_MS = 30_000;
14
+
15
+ let pollTimer: ReturnType<typeof setInterval> | null = null;
16
+ let dispatchInFlight = false;
17
+
18
+ type StoredQueuedMessage = {
19
+ content: string;
20
+ options: Record<string, unknown>;
21
+ attachments: unknown[];
22
+ };
23
+
24
+ function readOptions(raw: string): Record<string, unknown> {
25
+ try {
26
+ const parsed: unknown = JSON.parse(raw);
27
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
28
+ ? parsed as Record<string, unknown>
29
+ : {};
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+
35
+ function readQueuedMessage(value: unknown): StoredQueuedMessage | null {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
37
+ return null;
38
+ }
39
+ const record = value as Record<string, unknown>;
40
+ const content = typeof record.content === 'string' ? record.content : '';
41
+ const attachments = Array.isArray(record.attachments)
42
+ ? record.attachments
43
+ : Array.isArray(record.images)
44
+ ? record.images
45
+ : [];
46
+ if (!content.trim() && attachments.length === 0) {
47
+ return null;
48
+ }
49
+ const options = record.options && typeof record.options === 'object' && !Array.isArray(record.options)
50
+ ? record.options as Record<string, unknown>
51
+ : {};
52
+ return { content, options, attachments };
53
+ }
54
+
55
+ async function sendClaimedQueuedMessage(
56
+ candidate: QueuedSessionMessageRecord,
57
+ runtime: ProviderRuntimeGateway,
58
+ ): Promise<void> {
59
+ const message = readQueuedMessage(candidate.queuedMessage);
60
+ if (!message) {
61
+ sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
62
+ return;
63
+ }
64
+
65
+ const result = await runDetachedChatTurn(
66
+ {
67
+ sessionId: candidate.sessionId,
68
+ userId: candidate.userId,
69
+ content: message.content,
70
+ options: { ...message.options, attachments: message.attachments },
71
+ },
72
+ { runtime },
73
+ );
74
+
75
+ // The registry check and run reservation are separate operations. If a run
76
+ // wins that tiny race, put the turn back so the next poll tries again.
77
+ if (!result.started && result.error === 'A run was already in progress for this session.') {
78
+ sessionDraftsDb.restoreQueuedMessage(candidate);
79
+ return;
80
+ }
81
+ sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
82
+ }
83
+
84
+ /**
85
+ * Whether a queued turn has to keep waiting for the session.
86
+ *
87
+ * A turn that ends with background work still going (a backgrounded command,
88
+ * agent or workflow) is marked completed, but its CLI process stays open so
89
+ * that work can report back. A new turn replaces that process and stops the
90
+ * work, which is why the composer asks before an interactive send. A queued
91
+ * turn was promised to go "when this finishes", so it waits for that work too.
92
+ */
93
+ function isSessionBusy(sessionId: string, runtime: ProviderRuntimeGateway): boolean {
94
+ return chatRunRegistry.isProcessing(sessionId) || runtime.hasBackgroundWork(sessionId);
95
+ }
96
+
97
+ /** Sends every persisted queued turn whose session is currently idle. */
98
+ export async function dispatchQueuedMessages(runtime: ProviderRuntimeGateway): Promise<number> {
99
+ const candidates = sessionDraftsDb.listQueuedMessages();
100
+ let claimed = 0;
101
+
102
+ await Promise.all(candidates.map(async (candidate) => {
103
+ if (isSessionBusy(candidate.sessionId, runtime)) {
104
+ return;
105
+ }
106
+ if (!sessionDraftsDb.claimQueuedMessage(candidate)) {
107
+ return;
108
+ }
109
+ claimed += 1;
110
+ await sendClaimedQueuedMessage(candidate, runtime);
111
+ }));
112
+
113
+ return claimed;
114
+ }
115
+
116
+ async function sendClaimedMessage(
117
+ row: ScheduledMessageRow,
118
+ runtime: ProviderRuntimeGateway,
119
+ ): Promise<void> {
120
+ try {
121
+ const result = await runDetachedChatTurn(
122
+ {
123
+ sessionId: row.session_id,
124
+ userId: row.user_id,
125
+ content: row.content,
126
+ options: readOptions(row.options),
127
+ // The user picked this time on purpose; a run that happens to be going
128
+ // is aborted so the scheduled message lands when it was due, instead
129
+ // of being recorded as "not sent — session was busy".
130
+ interruptActiveRun: true,
131
+ },
132
+ { runtime },
133
+ );
134
+
135
+ // Recorded rather than retried, and recorded whether the run never started
136
+ // (deleted session, unavailable provider) or started and then failed.
137
+ // Silently dropping a message the user scheduled is worse than telling
138
+ // them it did not go.
139
+ if (!result.started || result.error) {
140
+ scheduledMessagesDb.markFailed(row.id, result.error ?? 'The session was unavailable when this was due.');
141
+ }
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ scheduledMessagesDb.markFailed(row.id, message);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Sends every message whose time has come.
150
+ *
151
+ * Exported so a test can drive one pass without waiting on the timer.
152
+ */
153
+ export async function dispatchDueScheduledMessages(
154
+ runtime: ProviderRuntimeGateway,
155
+ now: Date = new Date(),
156
+ ): Promise<number> {
157
+ // Claimed before any of them runs, so a long turn cannot let the next poll
158
+ // pick the same message up again.
159
+ const due = scheduledMessagesDb.claimDue(now);
160
+ if (due.length === 0) {
161
+ return 0;
162
+ }
163
+
164
+ // Sequentially: a session can only have one run at a time, and two due
165
+ // messages for the same session must not race each other into it.
166
+ for (const row of due) {
167
+ await sendClaimedMessage(row, runtime);
168
+ }
169
+
170
+ return due.length;
171
+ }
172
+
173
+ /**
174
+ * Starts the poll that sends scheduled messages.
175
+ *
176
+ * The schedule lives in the database, so a message stays scheduled across a
177
+ * restart and one that came due while the server was down is sent on the first
178
+ * poll after it comes back, rather than being skipped.
179
+ */
180
+ export function initializeScheduledMessageDispatcher(runtime: ProviderRuntimeGateway): void {
181
+ if (pollTimer) {
182
+ return;
183
+ }
184
+
185
+ const poll = () => {
186
+ // A pass that overruns the interval must not be started again underneath
187
+ // itself; the claim is transactional but the runs are not.
188
+ if (dispatchInFlight) {
189
+ return;
190
+ }
191
+ dispatchInFlight = true;
192
+ void dispatchDueScheduledMessages(runtime)
193
+ .then(() => dispatchQueuedMessages(runtime))
194
+ .catch((error: unknown) => {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ console.error('[ScheduledMessages] Dispatch pass failed', { error: message });
197
+ })
198
+ .finally(() => {
199
+ dispatchInFlight = false;
200
+ });
201
+ };
202
+
203
+ pollTimer = setInterval(poll, POLL_INTERVAL_MS);
204
+ // Never keep the process alive just to poll for scheduled messages.
205
+ pollTimer.unref?.();
206
+
207
+ // Catch up on anything that came due while the server was not running.
208
+ poll();
209
+ }
210
+
211
+ export function closeScheduledMessageDispatcher(): void {
212
+ if (pollTimer) {
213
+ clearInterval(pollTimer);
214
+ pollTimer = null;
215
+ }
216
+ }