@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.
- package/README.md +269 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-CJntskwA.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-CIfH4uMN.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-_IpEkukg.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-BttxrpgZ.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-g51dSRr1.js} +1 -1
- package/dist/assets/channel-C5xU9jMw.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-D_-LbQUa.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-BtlHzwaO.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-UzkpvpAT.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-BKwx5Gag.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-D_KEBH5f.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-rK2DGyto.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CeBl_N9i.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-BiQLWVIr.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-C1knVGV5.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-CtKbJkvF.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DjX8y40Z.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-DM6YeRba.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-wZJ-vy5B.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-BzP0ejkH.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-BTL_j23J.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-Cf1Norgu.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-DPiUEKr-.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-CTcTpiz5.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-DPMSdYm0.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-7yvD6uiF.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CfgeDoV6.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-B27ng5zu.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-CcC6b_7t.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-BX5kPGrO.js} +1 -1
- package/dist/assets/index-BTDHQ6Id.css +32 -0
- package/dist/assets/index-sFfz8iBR.js +4931 -0
- package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-CUP-SlN4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-BTTDhOpX.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-ARXcc61m.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-BieMpqMA.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-61vxjRnP.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-nhnwkfQx.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-Cm6x5vT1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-CcYBq_9H.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-CfvIjfzo.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-SgYoWPNb.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-D2Mu5E_f.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Azwe_k4Q.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-DN1Ju3PA.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-Bj4qC34S.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-Co4PdklG.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-nOvL9dRC.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-CCK3wMid.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-BC29RNFm.js} +1 -1
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-pZlZCtEo.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-De8k2-zM.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-Bk8ykH0w.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +7 -3
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +2 -2
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +1 -1
- package/dist-server/server/modules/database/connection.js.map +1 -1
- package/dist-server/server/modules/database/index.js +5 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +32 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
- package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +92 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +168 -38
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +24 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/index.js +5 -0
- package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
- package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
- package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
- package/dist-server/server/modules/user/user.module.js +10 -1
- package/dist-server/server/modules/user/user.module.js.map +1 -1
- package/dist-server/server/modules/user/user.routes.js +44 -0
- package/dist-server/server/modules/user/user.routes.js.map +1 -1
- package/dist-server/server/modules/user/user.service.js +75 -0
- package/dist-server/server/modules/user/user.service.js.map +1 -1
- package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
- package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +7 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +22 -5
- package/dist-server/server/shared/claude-cli-path.js.map +1 -1
- package/dist-server/server/shared/index.js +3 -0
- package/dist-server/server/shared/index.js.map +1 -0
- package/dist-server/server/shared/message-unification.js +376 -0
- package/dist-server/server/shared/message-unification.js.map +1 -0
- package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
- package/dist-server/server/shared/tests/message-unification.test.js +174 -0
- package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +34 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +19 -16
- package/server/index.ts +11 -4
- package/server/modules/agent/agent.routes.ts +2 -2
- package/server/modules/agent/tests/agent.routes.test.ts +33 -1
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/database/connection.ts +2 -1
- package/server/modules/database/index.ts +10 -0
- package/server/modules/database/migrations.ts +37 -0
- package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
- package/server/modules/database/repositories/session-drafts.db.ts +171 -0
- package/server/modules/database/repositories/sessions.db.ts +196 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +96 -0
- package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
- package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
- package/server/modules/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
- package/server/modules/providers/list/claude/claude-runtime.provider.js +188 -39
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
- package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
- package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
- package/server/modules/providers/provider.routes.ts +31 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-history-cache.service.ts +139 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
- package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
- package/server/modules/providers/services/sessions.service.ts +201 -9
- package/server/modules/providers/tests/claude-auth.test.ts +58 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
- package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
- package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
- package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
- package/server/modules/providers/tests/opencode-models.test.ts +166 -8
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
- package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
- package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
- package/server/modules/providers/tests/sessions.service.test.ts +67 -1
- package/server/modules/providers/tests/skills.test.ts +21 -5
- package/server/modules/scheduled-messages/index.ts +8 -0
- package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
- package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
- package/server/modules/user/tests/user.service.test.ts +90 -0
- package/server/modules/user/user.module.ts +10 -1
- package/server/modules/user/user.routes.ts +44 -0
- package/server/modules/user/user.service.ts +105 -0
- package/server/modules/voice/tests/voice.service.test.ts +3 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
- package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +62 -0
- package/server/shared/message-unification.ts +449 -0
- package/server/shared/tests/claude-cli-path.test.ts +58 -1
- package/server/shared/tests/message-unification.test.ts +218 -0
- package/server/shared/types.ts +123 -2
- package/server/shared/utils.ts +40 -0
- package/dist/assets/channel-_falYbU1.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
- package/dist/assets/index-CzPLBvXT.css +0 -32
- package/dist/assets/index-DFkBBuSA.js +0 -1549
- package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
- 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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
37
|
-
if (client.readyState ===
|
|
47
|
+
connectedClients.forEach((client) => {
|
|
48
|
+
if (client.readyState === WS_OPEN_STATE) client.send(message);
|
|
38
49
|
});
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
function broadcastTaskMasterProjectUpdate(
|
|
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(
|
|
52
|
-
broadcastTaskMasterUpdate(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|