@cloudcli-ai/cloudcli 1.37.2 → 1.37.3

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 (328) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-CJntskwA.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-CIfH4uMN.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-_IpEkukg.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-BttxrpgZ.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-g51dSRr1.js} +1 -1
  7. package/dist/assets/channel-C5xU9jMw.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-D_-LbQUa.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-BtlHzwaO.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-UzkpvpAT.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-BKwx5Gag.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-D_KEBH5f.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-rK2DGyto.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CeBl_N9i.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-BiQLWVIr.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-C1knVGV5.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-CtKbJkvF.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DjX8y40Z.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-DM6YeRba.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-wZJ-vy5B.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-BzP0ejkH.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-BTL_j23J.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-Cf1Norgu.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-DPiUEKr-.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-CTcTpiz5.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-DPMSdYm0.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-7yvD6uiF.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CfgeDoV6.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-B27ng5zu.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-CcC6b_7t.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-BX5kPGrO.js} +1 -1
  34. package/dist/assets/index-BTDHQ6Id.css +32 -0
  35. package/dist/assets/index-sFfz8iBR.js +4931 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-CUP-SlN4.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-BTTDhOpX.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-ARXcc61m.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-BieMpqMA.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-61vxjRnP.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-nhnwkfQx.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-Cm6x5vT1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-CcYBq_9H.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-CfvIjfzo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-SgYoWPNb.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-D2Mu5E_f.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Azwe_k4Q.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-DN1Ju3PA.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-Bj4qC34S.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-Co4PdklG.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-nOvL9dRC.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-CCK3wMid.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-BC29RNFm.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-pZlZCtEo.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-De8k2-zM.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-Bk8ykH0w.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +7 -3
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +2 -2
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
  67. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  68. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  69. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  70. package/dist-server/server/modules/database/connection.js +1 -1
  71. package/dist-server/server/modules/database/connection.js.map +1 -1
  72. package/dist-server/server/modules/database/index.js +5 -0
  73. package/dist-server/server/modules/database/index.js.map +1 -1
  74. package/dist-server/server/modules/database/migrations.js +32 -1
  75. package/dist-server/server/modules/database/migrations.js.map +1 -1
  76. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  77. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  78. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  79. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  80. package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
  81. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  83. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  84. package/dist-server/server/modules/database/schema.js +92 -0
  85. package/dist-server/server/modules/database/schema.js.map +1 -1
  86. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  87. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  88. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  89. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  90. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  91. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  92. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  93. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  94. package/dist-server/server/modules/providers/index.js +3 -0
  95. package/dist-server/server/modules/providers/index.js.map +1 -1
  96. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
  97. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  98. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  99. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
  101. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  102. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +168 -38
  103. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  104. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
  105. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  106. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  108. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  109. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  111. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  112. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  113. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  114. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  119. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
  121. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  123. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  125. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
  128. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/provider.routes.js +24 -0
  130. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  131. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
  132. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  134. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  135. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  136. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  138. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  140. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
  142. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
  144. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  145. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  146. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  147. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
  148. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  150. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  151. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  152. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  154. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  156. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  158. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
  160. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
  162. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  163. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  164. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  165. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  166. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  167. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  168. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  169. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  170. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  172. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  173. package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
  174. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  176. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  177. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  178. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  179. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  180. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  181. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  182. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  183. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  184. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  185. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  186. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  187. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  188. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  189. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  190. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  191. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  192. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  193. package/dist-server/server/modules/user/user.module.js +10 -1
  194. package/dist-server/server/modules/user/user.module.js.map +1 -1
  195. package/dist-server/server/modules/user/user.routes.js +44 -0
  196. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  197. package/dist-server/server/modules/user/user.service.js +75 -0
  198. package/dist-server/server/modules/user/user.service.js.map +1 -1
  199. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  200. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  201. package/dist-server/server/modules/websocket/index.js +7 -0
  202. package/dist-server/server/modules/websocket/index.js.map +1 -1
  203. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
  204. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  205. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  206. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  207. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
  208. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  209. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  210. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  211. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
  212. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  213. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  214. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  215. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  216. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  217. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  218. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  219. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
  220. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  221. package/dist-server/server/shared/claude-cli-path.js +22 -5
  222. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  223. package/dist-server/server/shared/index.js +3 -0
  224. package/dist-server/server/shared/index.js.map +1 -0
  225. package/dist-server/server/shared/message-unification.js +376 -0
  226. package/dist-server/server/shared/message-unification.js.map +1 -0
  227. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  228. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  229. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  230. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  231. package/dist-server/server/shared/utils.js +34 -0
  232. package/dist-server/server/shared/utils.js.map +1 -1
  233. package/package.json +19 -16
  234. package/server/index.ts +11 -4
  235. package/server/modules/agent/agent.routes.ts +2 -2
  236. package/server/modules/agent/tests/agent.routes.test.ts +33 -1
  237. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  238. package/server/modules/database/connection.ts +2 -1
  239. package/server/modules/database/index.ts +10 -0
  240. package/server/modules/database/migrations.ts +37 -0
  241. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  242. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  243. package/server/modules/database/repositories/sessions.db.ts +196 -3
  244. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  245. package/server/modules/database/schema.ts +96 -0
  246. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  247. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  248. package/server/modules/projects/services/project-delete.service.ts +13 -1
  249. package/server/modules/projects/services/project-star.service.ts +2 -2
  250. package/server/modules/providers/index.ts +4 -0
  251. package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
  252. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  253. package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
  254. package/server/modules/providers/list/claude/claude-runtime.provider.js +188 -39
  255. package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
  256. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  257. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  258. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  259. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  260. package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
  261. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
  262. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  263. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
  264. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  265. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  266. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  267. package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
  268. package/server/modules/providers/provider.routes.ts +31 -0
  269. package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
  270. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  271. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  272. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  273. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  274. package/server/modules/providers/services/sessions.service.ts +201 -9
  275. package/server/modules/providers/tests/claude-auth.test.ts +58 -0
  276. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  277. package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
  278. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  279. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  280. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  281. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  282. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  283. package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
  284. package/server/modules/providers/tests/opencode-models.test.ts +166 -8
  285. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  286. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  287. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  288. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  289. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  290. package/server/modules/providers/tests/sessions.service.test.ts +67 -1
  291. package/server/modules/providers/tests/skills.test.ts +21 -5
  292. package/server/modules/scheduled-messages/index.ts +8 -0
  293. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  294. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  295. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  296. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  297. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  298. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  299. package/server/modules/user/tests/user.service.test.ts +90 -0
  300. package/server/modules/user/user.module.ts +10 -1
  301. package/server/modules/user/user.routes.ts +44 -0
  302. package/server/modules/user/user.service.ts +105 -0
  303. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  304. package/server/modules/websocket/index.ts +8 -0
  305. package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
  306. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  307. package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
  308. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  309. package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
  310. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  311. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  312. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  313. package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
  314. package/server/shared/claude-cli-path.ts +24 -6
  315. package/server/shared/index.ts +3 -0
  316. package/server/shared/interfaces.ts +62 -0
  317. package/server/shared/message-unification.ts +449 -0
  318. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  319. package/server/shared/tests/message-unification.test.ts +218 -0
  320. package/server/shared/types.ts +123 -2
  321. package/server/shared/utils.ts +40 -0
  322. package/dist/assets/channel-_falYbU1.js +0 -1
  323. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  324. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  325. package/dist/assets/index-CzPLBvXT.css +0 -32
  326. package/dist/assets/index-DFkBBuSA.js +0 -1549
  327. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  328. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -0,0 +1,310 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, scheduledMessagesDb, sessionDraftsDb, sessionsDb, userDb } from '@/modules/database/index.js';
8
+ import { dispatchDueScheduledMessages, dispatchQueuedMessages } from '@/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js';
9
+ import { scheduledMessagesService } from '@/modules/scheduled-messages/services/scheduled-messages.service.js';
10
+ import { chatRunRegistry } from '@/modules/websocket/index.js';
11
+
12
+ const SESSION_ID = 'scheduled-session';
13
+
14
+ async function withIsolatedDatabase(runTest: (userId: number) => void | Promise<void>): Promise<void> {
15
+ const previousDatabasePath = process.env.DATABASE_PATH;
16
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'scheduled-messages-'));
17
+
18
+ closeConnection();
19
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
20
+ await initializeDatabase();
21
+
22
+ try {
23
+ const user = userDb.createUser('scheduler', 'hash');
24
+ sessionsDb.createAppSession(SESSION_ID, 'claude', tempDirectory, 'Scheduled session');
25
+ await runTest(Number(user.id));
26
+ } finally {
27
+ chatRunRegistry.clearAll();
28
+ closeConnection();
29
+ if (previousDatabasePath === undefined) {
30
+ delete process.env.DATABASE_PATH;
31
+ } else {
32
+ process.env.DATABASE_PATH = previousDatabasePath;
33
+ }
34
+ await rm(tempDirectory, { recursive: true, force: true });
35
+ }
36
+ }
37
+
38
+ type RunCall = { provider: string; command: string; options: Record<string, unknown> };
39
+
40
+ function createRuntime(runs: RunCall[], behaviour: 'ok' | 'throw' = 'ok', aborts: string[] = []) {
41
+ return {
42
+ hasRuntime: () => true,
43
+ run: async (provider: string, command: string, options: Record<string, unknown>) => {
44
+ if (behaviour === 'throw') {
45
+ throw new Error('provider exploded');
46
+ }
47
+ runs.push({ provider, command, options });
48
+ },
49
+ abort: async (_provider: string, sessionId: string) => {
50
+ aborts.push(sessionId);
51
+ return true;
52
+ },
53
+ } as never;
54
+ }
55
+
56
+ test('a message due in the past is sent on the next pass, not skipped', async () => {
57
+ await withIsolatedDatabase(async (userId) => {
58
+ // The server was down when this came due.
59
+ scheduledMessagesService.schedule({
60
+ userId,
61
+ sessionId: SESSION_ID,
62
+ content: 'run the nightly checks',
63
+ scheduledFor: new Date(Date.now() - 60_000).toISOString(),
64
+ });
65
+
66
+ const runs: RunCall[] = [];
67
+ const sent = await dispatchDueScheduledMessages(createRuntime(runs));
68
+
69
+ assert.equal(sent, 1);
70
+ assert.equal(runs.length, 1);
71
+ assert.equal(runs[0].command, 'run the nightly checks');
72
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'sent');
73
+ });
74
+ });
75
+
76
+ test('a queued message is sent by the server without a browser connection', async () => {
77
+ await withIsolatedDatabase(async (userId) => {
78
+ sessionDraftsDb.saveDraft(userId, SESSION_ID, {
79
+ text: '',
80
+ queuedMessage: {
81
+ content: 'continue on the VPS',
82
+ options: { model: 'claude-opus-5' },
83
+ attachments: [{ path: '/tmp/upload.png' }],
84
+ },
85
+ });
86
+
87
+ const runs: RunCall[] = [];
88
+ assert.equal(await dispatchQueuedMessages(createRuntime(runs)), 1);
89
+ assert.equal(runs.length, 1);
90
+ assert.equal(runs[0].command, 'continue on the VPS');
91
+ assert.equal(runs[0].options.model, 'claude-opus-5');
92
+ assert.deepEqual(runs[0].options.attachments, []);
93
+ assert.equal(sessionDraftsDb.getDrafts(userId).length, 0);
94
+ });
95
+ });
96
+
97
+ test('a queued message stays pending while its session is busy', async () => {
98
+ await withIsolatedDatabase(async (userId) => {
99
+ sessionDraftsDb.saveDraft(userId, SESSION_ID, {
100
+ text: '',
101
+ queuedMessage: { content: 'send after this run' },
102
+ });
103
+ chatRunRegistry.startRun({
104
+ appSessionId: SESSION_ID,
105
+ provider: 'claude',
106
+ providerSessionId: null,
107
+ connection: null,
108
+ userId,
109
+ });
110
+
111
+ const runs: RunCall[] = [];
112
+ assert.equal(await dispatchQueuedMessages(createRuntime(runs)), 0);
113
+ assert.equal(runs.length, 0);
114
+ assert.deepEqual(sessionDraftsDb.getDrafts(userId)[0]?.queuedMessage, {
115
+ content: 'send after this run',
116
+ });
117
+ });
118
+ });
119
+
120
+ test('a due message interrupts a run in progress instead of failing', async () => {
121
+ await withIsolatedDatabase(async (userId) => {
122
+ scheduledMessagesService.schedule({
123
+ userId,
124
+ sessionId: SESSION_ID,
125
+ content: 'the schedule wins',
126
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
127
+ });
128
+ chatRunRegistry.startRun({
129
+ appSessionId: SESSION_ID,
130
+ provider: 'claude',
131
+ providerSessionId: null,
132
+ connection: null,
133
+ userId,
134
+ });
135
+
136
+ const runs: RunCall[] = [];
137
+ const aborts: string[] = [];
138
+ assert.equal(await dispatchDueScheduledMessages(createRuntime(runs, 'ok', aborts)), 1);
139
+
140
+ assert.deepEqual(aborts, [SESSION_ID]);
141
+ assert.equal(runs.length, 1);
142
+ assert.equal(runs[0].command, 'the schedule wins');
143
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'sent');
144
+ });
145
+ });
146
+
147
+ test('a message that is not due yet is left alone', async () => {
148
+ await withIsolatedDatabase(async (userId) => {
149
+ scheduledMessagesService.schedule({
150
+ userId,
151
+ sessionId: SESSION_ID,
152
+ content: 'later',
153
+ scheduledFor: new Date(Date.now() + 3_600_000).toISOString(),
154
+ });
155
+
156
+ const runs: RunCall[] = [];
157
+ assert.equal(await dispatchDueScheduledMessages(createRuntime(runs)), 0);
158
+ assert.equal(runs.length, 0);
159
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'pending');
160
+ });
161
+ });
162
+
163
+ test('a due message is claimed once, so overlapping passes cannot double-send it', async () => {
164
+ await withIsolatedDatabase(async (userId) => {
165
+ scheduledMessagesService.schedule({
166
+ userId,
167
+ sessionId: SESSION_ID,
168
+ content: 'only once',
169
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
170
+ });
171
+
172
+ const runs: RunCall[] = [];
173
+ const runtime = createRuntime(runs);
174
+ await Promise.all([
175
+ dispatchDueScheduledMessages(runtime),
176
+ dispatchDueScheduledMessages(runtime),
177
+ ]);
178
+
179
+ assert.equal(runs.length, 1);
180
+ });
181
+ });
182
+
183
+ test('the composer settings it was scheduled with travel with it', async () => {
184
+ await withIsolatedDatabase(async (userId) => {
185
+ scheduledMessagesService.schedule({
186
+ userId,
187
+ sessionId: SESSION_ID,
188
+ content: 'with options',
189
+ options: { model: 'claude-opus-5', permissionMode: 'plan' },
190
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
191
+ });
192
+
193
+ const runs: RunCall[] = [];
194
+ await dispatchDueScheduledMessages(createRuntime(runs));
195
+
196
+ assert.equal(runs[0].options.model, 'claude-opus-5');
197
+ assert.equal(runs[0].options.permissionMode, 'plan');
198
+ });
199
+ });
200
+
201
+ test('a provider failure is recorded on the message instead of vanishing', async () => {
202
+ await withIsolatedDatabase(async (userId) => {
203
+ scheduledMessagesService.schedule({
204
+ userId,
205
+ sessionId: SESSION_ID,
206
+ content: 'will fail',
207
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
208
+ });
209
+
210
+ await dispatchDueScheduledMessages(createRuntime([], 'throw'));
211
+
212
+ const row = scheduledMessagesDb.listForSession(userId, SESSION_ID)[0];
213
+ assert.equal(row.status, 'failed');
214
+ assert.match(row.failure_reason ?? '', /provider exploded/);
215
+ });
216
+ });
217
+
218
+ test('a cancelled message never fires', async () => {
219
+ await withIsolatedDatabase(async (userId) => {
220
+ const scheduled = scheduledMessagesService.schedule({
221
+ userId,
222
+ sessionId: SESSION_ID,
223
+ content: 'never mind',
224
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
225
+ });
226
+ scheduledMessagesService.cancel(userId, scheduled.id);
227
+
228
+ const runs: RunCall[] = [];
229
+ assert.equal(await dispatchDueScheduledMessages(createRuntime(runs)), 0);
230
+ assert.equal(runs.length, 0);
231
+ });
232
+ });
233
+
234
+ test('a failed message can be dismissed, and stays dismissed', async () => {
235
+ await withIsolatedDatabase(async (userId) => {
236
+ const scheduled = scheduledMessagesService.schedule({
237
+ userId,
238
+ sessionId: SESSION_ID,
239
+ content: 'will fail',
240
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
241
+ });
242
+ await dispatchDueScheduledMessages(createRuntime([], 'throw'));
243
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'failed');
244
+
245
+ scheduledMessagesService.cancel(userId, scheduled.id);
246
+
247
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'cancelled');
248
+ });
249
+ });
250
+
251
+ test('cancelling something that already fired is refused', async () => {
252
+ await withIsolatedDatabase(async (userId) => {
253
+ const scheduled = scheduledMessagesService.schedule({
254
+ userId,
255
+ sessionId: SESSION_ID,
256
+ content: 'gone',
257
+ scheduledFor: new Date(Date.now() - 1_000).toISOString(),
258
+ });
259
+ await dispatchDueScheduledMessages(createRuntime([]));
260
+
261
+ assert.throws(
262
+ () => scheduledMessagesService.cancel(userId, scheduled.id),
263
+ (error: Error & { code?: string }) => error.code === 'SCHEDULED_MESSAGE_NOT_PENDING',
264
+ );
265
+ });
266
+ });
267
+
268
+ test('one user cannot cancel another user\'s scheduled message', async () => {
269
+ await withIsolatedDatabase(async (userId) => {
270
+ const scheduled = scheduledMessagesService.schedule({
271
+ userId,
272
+ sessionId: SESSION_ID,
273
+ content: 'mine',
274
+ scheduledFor: new Date(Date.now() + 3_600_000).toISOString(),
275
+ });
276
+
277
+ assert.throws(
278
+ () => scheduledMessagesService.cancel(userId + 1, scheduled.id),
279
+ (error: Error & { code?: string }) => error.code === 'SCHEDULED_MESSAGE_NOT_PENDING',
280
+ );
281
+ assert.equal(scheduledMessagesDb.listForSession(userId, SESSION_ID)[0].status, 'pending');
282
+ });
283
+ });
284
+
285
+ test('scheduling validates its input', async () => {
286
+ await withIsolatedDatabase(async (userId) => {
287
+ const base = { userId, sessionId: SESSION_ID, scheduledFor: new Date(Date.now() + 1000).toISOString() };
288
+
289
+ assert.throws(
290
+ () => scheduledMessagesService.schedule({ ...base, content: ' ' }),
291
+ (error: Error & { code?: string }) => error.code === 'CONTENT_REQUIRED',
292
+ );
293
+ assert.throws(
294
+ () => scheduledMessagesService.schedule({ ...base, content: 'hi', scheduledFor: 'not a date' }),
295
+ (error: Error & { code?: string }) => error.code === 'INVALID_SCHEDULE_TIME',
296
+ );
297
+ assert.throws(
298
+ () => scheduledMessagesService.schedule({
299
+ ...base,
300
+ content: 'hi',
301
+ scheduledFor: new Date(Date.now() + 400 * 24 * 3600 * 1000).toISOString(),
302
+ }),
303
+ (error: Error & { code?: string }) => error.code === 'SCHEDULE_TOO_FAR_AHEAD',
304
+ );
305
+ assert.throws(
306
+ () => scheduledMessagesService.schedule({ ...base, sessionId: 'nope', content: 'hi' }),
307
+ (error: Error & { code?: string }) => error.code === 'SESSION_NOT_FOUND',
308
+ );
309
+ });
310
+ });
@@ -13,6 +13,8 @@ import path from 'path';
13
13
 
14
14
  import express from 'express';
15
15
 
16
+ import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
17
+
16
18
  import type { createTaskmasterService } from './taskmaster.service.js';
17
19
  // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution — required
18
20
  // here since task-master/npx are .cmd shims on Windows.
@@ -25,22 +27,30 @@ type TaskmasterRouterDependencies = {
25
27
  taskmasterService: ReturnType<typeof createTaskmasterService>;
26
28
  };
27
29
 
28
- function broadcastTaskMasterUpdate(wss, type, projectId, payloadKey, payload) {
29
- if (!wss || !projectId) return;
30
+ /**
31
+ * These frames are `type`-keyed and only TaskMasterContext, which lives on the
32
+ * `/ws` chat socket, knows what to do with them. Broadcasting over the raw
33
+ * `wss.clients` set instead delivered them to every `/shell`, `/plugin-ws` and
34
+ * `/desktop-notifications` socket as well, where they were parsed and dropped —
35
+ * and, on `/plugin-ws`, handed to third-party plugin frontends that have no
36
+ * business seeing them. `connectedClients` is the `/ws`-only set every other
37
+ * broadcast in the server already uses.
38
+ */
39
+ function broadcastTaskMasterUpdate(type, projectId, payloadKey, payload) {
40
+ if (!projectId) return;
30
41
  const message = JSON.stringify({
31
42
  type,
32
43
  projectId,
33
44
  ...(payload === undefined ? {} : { [payloadKey]: payload }),
34
45
  timestamp: new Date().toISOString(),
35
46
  });
36
- wss.clients.forEach((client) => {
37
- if (client.readyState === 1) client.send(message);
47
+ connectedClients.forEach((client) => {
48
+ if (client.readyState === WS_OPEN_STATE) client.send(message);
38
49
  });
39
50
  }
40
51
 
41
- function broadcastTaskMasterProjectUpdate(wss, projectId, taskMasterData) {
52
+ function broadcastTaskMasterProjectUpdate(projectId, taskMasterData) {
42
53
  broadcastTaskMasterUpdate(
43
- wss,
44
54
  'taskmaster-project-updated',
45
55
  projectId,
46
56
  'taskMasterData',
@@ -48,8 +58,8 @@ function broadcastTaskMasterProjectUpdate(wss, projectId, taskMasterData) {
48
58
  );
49
59
  }
50
60
 
51
- function broadcastTaskMasterTasksUpdate(wss, projectId, tasksData) {
52
- broadcastTaskMasterUpdate(wss, 'taskmaster-tasks-updated', projectId, 'tasksData', tasksData);
61
+ function broadcastTaskMasterTasksUpdate(projectId, tasksData) {
62
+ broadcastTaskMasterUpdate('taskmaster-tasks-updated', projectId, 'tasksData', tasksData);
53
63
  }
54
64
 
55
65
  /**
@@ -596,13 +606,7 @@ export function createTaskmasterRouter(dependencies: TaskmasterRouterDependencie
596
606
  // Broadcast TaskMaster project update via WebSocket. The
597
607
  // WebSocket payload keeps using `projectId` so the frontend
598
608
  // can match notifications against the current selection.
599
- if (req.app.locals.wss) {
600
- broadcastTaskMasterProjectUpdate(
601
- req.app.locals.wss,
602
- projectId,
603
- { hasTaskmaster: true, status: 'initialized' }
604
- );
605
- }
609
+ broadcastTaskMasterProjectUpdate(projectId, { hasTaskmaster: true, status: 'initialized' });
606
610
 
607
611
  res.json({
608
612
  projectId,
@@ -688,12 +692,7 @@ export function createTaskmasterRouter(dependencies: TaskmasterRouterDependencie
688
692
  if (code === 0) {
689
693
  // Broadcast task update via WebSocket using the projectId so
690
694
  // clients subscribed to this project get notified immediately.
691
- if (req.app.locals.wss) {
692
- broadcastTaskMasterTasksUpdate(
693
- req.app.locals.wss,
694
- projectId
695
- );
696
- }
695
+ broadcastTaskMasterTasksUpdate(projectId);
697
696
 
698
697
  res.json({
699
698
  projectId,
@@ -748,9 +747,7 @@ export function createTaskmasterRouter(dependencies: TaskmasterRouterDependencie
748
747
  }, ({ code, error, stdout, stderr }) => {
749
748
  if (code === 0) {
750
749
  // Broadcast task update via WebSocket
751
- if (req.app.locals.wss) {
752
- broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectId);
753
- }
750
+ broadcastTaskMasterTasksUpdate(projectId);
754
751
 
755
752
  res.json({
756
753
  projectId,
@@ -787,9 +784,7 @@ export function createTaskmasterRouter(dependencies: TaskmasterRouterDependencie
787
784
  }, ({ code, error, stdout, stderr }) => {
788
785
  if (code === 0) {
789
786
  // Broadcast task update via WebSocket
790
- if (req.app.locals.wss) {
791
- broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectId);
792
- }
787
+ broadcastTaskMasterTasksUpdate(projectId);
793
788
 
794
789
  res.json({
795
790
  projectId,
@@ -870,12 +865,7 @@ export function createTaskmasterRouter(dependencies: TaskmasterRouterDependencie
870
865
  }, ({ code, error, stdout, stderr }) => {
871
866
  if (code === 0) {
872
867
  // Broadcast task update via WebSocket
873
- if (req.app.locals.wss) {
874
- broadcastTaskMasterTasksUpdate(
875
- req.app.locals.wss,
876
- projectId
877
- );
878
- }
868
+ broadcastTaskMasterTasksUpdate(projectId);
879
869
 
880
870
  res.json({
881
871
  projectId,
@@ -6,6 +6,8 @@ import test from 'node:test';
6
6
 
7
7
  import express from 'express';
8
8
 
9
+ import { connectedClients } from '@/modules/websocket/index.js';
10
+
9
11
  import { createTaskmasterRouter } from '../taskmaster.routes.js';
10
12
 
11
13
  test('tasks route resolves project ids through the injected project adapter', async () => {
@@ -142,3 +144,58 @@ test('TaskMaster process errors use the endpoint failure response and settle onc
142
144
  await new Promise<void>((resolve) => server.close(() => resolve()));
143
145
  }
144
146
  });
147
+
148
+ test('TaskMaster broadcasts reach only the tracked chat clients', async () => {
149
+ const child = new EventEmitter() as EventEmitter & {
150
+ stdin: PassThrough;
151
+ stdout: PassThrough;
152
+ stderr: PassThrough;
153
+ };
154
+ child.stdin = new PassThrough();
155
+ child.stdout = new PassThrough();
156
+ child.stderr = new PassThrough();
157
+
158
+ const chatClient = { readyState: 1, frames: [] as string[], send(data: string) { this.frames.push(data); } };
159
+ const closingClient = { readyState: 3, frames: [] as string[], send(data: string) { this.frames.push(data); } };
160
+
161
+ const router = createTaskmasterRouter({
162
+ fileSystem: { constants: { F_OK: 0 } } as typeof import('node:fs'),
163
+ fileSystemPromises: {
164
+ access: async () => { throw new Error('not initialized'); },
165
+ } as unknown as typeof import('node:fs/promises'),
166
+ spawnProcess: (() => {
167
+ process.nextTick(() => child.emit('close', 0));
168
+ return child;
169
+ }) as unknown as Parameters<typeof createTaskmasterRouter>[0]['spawnProcess'],
170
+ resolveProjectPathById: () => '/workspace/project',
171
+ taskmasterService: {
172
+ detectMcpServer: async () => ({
173
+ hasMCPServer: false,
174
+ reason: 'Not configured',
175
+ hasConfig: false,
176
+ }),
177
+ },
178
+ });
179
+ const app = express().use('/api/taskmaster', router);
180
+ const server = app.listen(0, '127.0.0.1');
181
+ await once(server, 'listening');
182
+
183
+ connectedClients.add(chatClient as never);
184
+ connectedClients.add(closingClient as never);
185
+ try {
186
+ const address = server.address() as AddressInfo;
187
+ const response = await fetch(`http://127.0.0.1:${address.port}/api/taskmaster/init/project-1`, {
188
+ method: 'POST',
189
+ });
190
+ assert.equal(response.status, 200);
191
+
192
+ assert.equal(chatClient.frames.length, 1);
193
+ const frame = JSON.parse(chatClient.frames[0]) as Record<string, unknown>;
194
+ assert.equal(frame.type, 'taskmaster-project-updated');
195
+ assert.equal(frame.projectId, 'project-1');
196
+ assert.equal(closingClient.frames.length, 0);
197
+ } finally {
198
+ connectedClients.clear();
199
+ await new Promise<void>((resolve) => server.close(() => resolve()));
200
+ }
201
+ });
@@ -13,6 +13,15 @@ function createDependencies(overrides: Partial<UserDependencies> = {}): UserDepe
13
13
  completeOnboarding: () => undefined,
14
14
  hasCompletedOnboarding: () => false,
15
15
  },
16
+ preferences: {
17
+ getPreferences: () => ({}),
18
+ savePreferences: () => undefined,
19
+ },
20
+ drafts: {
21
+ getDrafts: () => [],
22
+ saveDraft: () => undefined,
23
+ deleteDraft: () => undefined,
24
+ },
16
25
  readSystemGitConfig: async () => ({ git_name: null, git_email: null }),
17
26
  applyGlobalGitConfig: async () => undefined,
18
27
  logInfo: () => undefined,
@@ -59,3 +68,84 @@ test('updateGitConfig persists valid input and invokes the Git adapter', async (
59
68
  'git:Alice:alice@example.com',
60
69
  ]);
61
70
  });
71
+
72
+ test('savePreferences forwards only the keys the client sent', () => {
73
+ const saved: Array<Record<string, unknown>> = [];
74
+ const service = createUserService(createDependencies({
75
+ preferences: {
76
+ getPreferences: () => ({ theme: 'dark' }),
77
+ savePreferences: (_id, updates) => saved.push(updates),
78
+ },
79
+ }));
80
+
81
+ const result = service.savePreferences(3, { theme: 'light' });
82
+
83
+ assert.deepEqual(saved, [{ theme: 'light' }]);
84
+ assert.deepEqual(result.preferences, { theme: 'dark' });
85
+ });
86
+
87
+ test('savePreferences rejects a non-object body', () => {
88
+ const service = createUserService(createDependencies());
89
+
90
+ assert.throws(() => service.savePreferences(3, ['theme']), /object/i);
91
+ assert.throws(() => service.savePreferences(3, 'theme'), /object/i);
92
+ });
93
+
94
+ test('savePreferences rejects an over-long preference key', () => {
95
+ const service = createUserService(createDependencies());
96
+
97
+ assert.throws(() => service.savePreferences(3, { ['k'.repeat(201)]: 1 }), /1-200/);
98
+ });
99
+
100
+ test('saveDraft stores the text and queued message under the given scope', () => {
101
+ const saved: unknown[][] = [];
102
+ const service = createUserService(createDependencies({
103
+ drafts: {
104
+ getDrafts: () => [],
105
+ saveDraft: (...args) => saved.push(args),
106
+ deleteDraft: () => undefined,
107
+ },
108
+ }));
109
+
110
+ service.saveDraft(5, 'session-1', {
111
+ scope: 'session-1',
112
+ text: 'half a thought',
113
+ queuedMessage: { content: 'later' },
114
+ });
115
+
116
+ assert.deepEqual(saved, [[5, 'session-1', {
117
+ text: 'half a thought',
118
+ queuedMessage: { content: 'later' },
119
+ }]]);
120
+ });
121
+
122
+ test('saveDraft defaults a missing queued message to null rather than dropping the row', () => {
123
+ const saved: unknown[][] = [];
124
+ const service = createUserService(createDependencies({
125
+ drafts: {
126
+ getDrafts: () => [],
127
+ saveDraft: (...args) => saved.push(args),
128
+ deleteDraft: () => undefined,
129
+ },
130
+ }));
131
+
132
+ service.saveDraft(5, 'session-1', { scope: 'session-1', text: 'typing' });
133
+
134
+ assert.deepEqual(saved, [[5, 'session-1', { text: 'typing', queuedMessage: null }]]);
135
+ });
136
+
137
+ test('saveDraft rejects a blank or over-long scope', () => {
138
+ const service = createUserService(createDependencies());
139
+
140
+ assert.throws(() => service.saveDraft(5, ' ', { text: 'x' }), /scope/i);
141
+ assert.throws(() => service.saveDraft(5, 'x'.repeat(201), { text: 'x' }), /scope/i);
142
+ });
143
+
144
+ test('saveDraft rejects text past the storage limit', () => {
145
+ const service = createUserService(createDependencies());
146
+
147
+ assert.throws(
148
+ () => service.saveDraft(5, 'session-1', { text: 'x'.repeat(100_001) }),
149
+ /too long/i,
150
+ );
151
+ });
@@ -1,6 +1,6 @@
1
1
  import spawn from 'cross-spawn';
2
2
 
3
- import { userDb } from '@/modules/database/index.js';
3
+ import { sessionDraftsDb, userDb, userPreferencesDb } from '@/modules/database/index.js';
4
4
 
5
5
  import { createUserRouter } from './user.routes.js';
6
6
  import { createUserService } from './user.service.js';
@@ -45,6 +45,15 @@ const userService = createUserService({
45
45
  completeOnboarding: (userId) => userDb.completeOnboarding(userId),
46
46
  hasCompletedOnboarding: (userId) => userDb.hasCompletedOnboarding(userId),
47
47
  },
48
+ preferences: {
49
+ getPreferences: (userId) => userPreferencesDb.getPreferences(userId),
50
+ savePreferences: (userId, updates) => userPreferencesDb.savePreferences(userId, updates),
51
+ },
52
+ drafts: {
53
+ getDrafts: (userId) => sessionDraftsDb.getDrafts(userId),
54
+ saveDraft: (userId, scope, draft) => sessionDraftsDb.saveDraft(userId, scope, draft),
55
+ deleteDraft: (userId, scope) => sessionDraftsDb.deleteDraft(userId, scope),
56
+ },
48
57
  readSystemGitConfig,
49
58
  applyGlobalGitConfig: async (gitName, gitEmail) => {
50
59
  await runGit(['config', '--global', 'user.name', gitName]);
@@ -46,5 +46,49 @@ export function createUserRouter(service: ReturnType<typeof createUserService>):
46
46
  }
47
47
  });
48
48
 
49
+ router.get('/preferences', (req, res, next) => {
50
+ try {
51
+ res.json(service.getPreferences(readUserId(req)));
52
+ } catch (error) {
53
+ next(error);
54
+ }
55
+ });
56
+
57
+ router.patch('/preferences', (req, res, next) => {
58
+ try {
59
+ res.json(service.savePreferences(readUserId(req), req.body));
60
+ } catch (error) {
61
+ next(error);
62
+ }
63
+ });
64
+
65
+ router.get('/drafts', (req, res, next) => {
66
+ try {
67
+ res.json(service.getDrafts(readUserId(req)));
68
+ } catch (error) {
69
+ next(error);
70
+ }
71
+ });
72
+
73
+ // The scope is a session id or `project:<id>`, so it is read from the body
74
+ // rather than the path: neither form is guaranteed to be URL-path-safe.
75
+ router.put('/drafts', (req, res, next) => {
76
+ try {
77
+ const body = req.body as { scope?: unknown };
78
+ res.json(service.saveDraft(readUserId(req), body?.scope, req.body));
79
+ } catch (error) {
80
+ next(error);
81
+ }
82
+ });
83
+
84
+ router.delete('/drafts', (req, res, next) => {
85
+ try {
86
+ const body = req.body as { scope?: unknown };
87
+ res.json(service.deleteDraft(readUserId(req), body?.scope));
88
+ } catch (error) {
89
+ next(error);
90
+ }
91
+ });
92
+
49
93
  return router;
50
94
  }