@cloudcli-ai/cloudcli 1.37.3 → 1.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
- package/dist/assets/index-CdjIFxM2.js +5004 -0
- package/dist/assets/index-DgafhGTF.css +32 -0
- package/dist/assets/{infoDiagram-6WML65LV-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
- package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +4 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +9 -2
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +175 -128
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +18 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +13 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.js +7 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +7 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
- package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
- package/dist-server/server/modules/git/git.routes.js +92 -5
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
- package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +13 -66
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
- package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +42 -8
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
- 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 +305 -16
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +686 -77
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -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 +11 -4
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +110 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +9 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +4 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1368 -12
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +32 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +79 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +19 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -20
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +420 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/message-unification.js +1 -1
- package/dist-server/server/shared/message-unification.js.map +1 -1
- package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +126 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/electron/desktopWindow.js +6 -0
- package/electron/viewHost.js +25 -2
- package/package.json +4 -4
- package/server/index.ts +5 -1
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +198 -140
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +18 -1
- package/server/modules/database/migrations.ts +14 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/sessions.db.ts +8 -0
- package/server/modules/database/schema.ts +7 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
- package/server/modules/file-tree/file-tree.module.ts +2 -1
- package/server/modules/file-tree/file-tree.service.ts +49 -7
- package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
- package/server/modules/git/git-branch-diff.service.ts +204 -0
- package/server/modules/git/git-branch.service.ts +2 -12
- package/server/modules/git/git.routes.ts +100 -6
- package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
- package/server/modules/projects/project-clone.routes.ts +142 -0
- package/server/modules/projects/projects.routes.ts +16 -81
- package/server/modules/projects/services/project-clone-request.service.ts +56 -0
- package/server/modules/projects/services/project-clone.service.ts +102 -34
- package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
- package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
- package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
- package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
- package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
- package/server/modules/providers/provider.routes.ts +131 -2
- package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
- package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +17 -1
- package/server/modules/providers/services/provider-models.service.ts +71 -4
- package/server/modules/providers/services/provider-runtime.service.ts +11 -0
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +13 -2
- package/server/modules/providers/services/sessions.service.ts +108 -8
- package/server/modules/providers/tests/claude-auth.test.ts +150 -0
- package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
- package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
- package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +100 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
- package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
- package/server/modules/providers/tests/sessions.service.test.ts +237 -1
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
- package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
- package/server/shared/interfaces.ts +29 -0
- package/server/shared/message-unification.ts +1 -1
- package/server/shared/tests/read-only-roots.test.ts +139 -0
- package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
- package/server/shared/types.ts +261 -7
- package/server/shared/utils.ts +136 -0
- package/dist/assets/channel-C5xU9jMw.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
- package/dist/assets/index-BTDHQ6Id.css +0 -32
- package/dist/assets/index-sFfz8iBR.js +0 -4931
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
|
@@ -4,6 +4,9 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
|
|
9
|
+
import { closeConnection, initializeDatabase } from '@/modules/database/index.js';
|
|
7
10
|
import {
|
|
8
11
|
OpenCodeProviderModels,
|
|
9
12
|
OPENCODE_PREDEFINED_MODELS,
|
|
@@ -197,3 +200,100 @@ test('OpenCode offers only models the install can route to', async () => {
|
|
|
197
200
|
},
|
|
198
201
|
);
|
|
199
202
|
});
|
|
203
|
+
|
|
204
|
+
const writeOpenCodeSessionDatabase = async (homeDir: string, rows: Array<Record<string, unknown>>) => {
|
|
205
|
+
const dbPath = path.join(homeDir, '.local', 'share', 'opencode', 'opencode.db');
|
|
206
|
+
await mkdir(path.dirname(dbPath), { recursive: true });
|
|
207
|
+
const db = new Database(dbPath);
|
|
208
|
+
try {
|
|
209
|
+
db.exec(`
|
|
210
|
+
CREATE TABLE session (
|
|
211
|
+
id text PRIMARY KEY,
|
|
212
|
+
model text,
|
|
213
|
+
agent text,
|
|
214
|
+
directory text,
|
|
215
|
+
time_created integer NOT NULL,
|
|
216
|
+
time_updated integer NOT NULL
|
|
217
|
+
);
|
|
218
|
+
`);
|
|
219
|
+
const insert = db.prepare(
|
|
220
|
+
'INSERT INTO session (id, model, agent, directory, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)',
|
|
221
|
+
);
|
|
222
|
+
for (const [index, row] of rows.entries()) {
|
|
223
|
+
insert.run(
|
|
224
|
+
row.id,
|
|
225
|
+
row.model,
|
|
226
|
+
row.agent ?? null,
|
|
227
|
+
row.directory ?? '/tmp/project',
|
|
228
|
+
row.timeCreated ?? 1700000000000,
|
|
229
|
+
row.timeUpdated ?? 1700000001000 + index,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
db.close();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
test('OpenCode composes the provider prefix into session model ids', async () => {
|
|
238
|
+
// The lookup translates the app session id through the sessions database,
|
|
239
|
+
// so point it at a throwaway file rather than the developer's real one.
|
|
240
|
+
const tempSessionsDirectory = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-model-'));
|
|
241
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
242
|
+
try {
|
|
243
|
+
closeConnection();
|
|
244
|
+
process.env.DATABASE_PATH = path.join(tempSessionsDirectory, 'auth.db');
|
|
245
|
+
await initializeDatabase();
|
|
246
|
+
// OpenCode stores the provider and model id in separate fields of the
|
|
247
|
+
// session row ({"id":"z-ai/glm-5.3-flash","providerID":"openrouter"}), but
|
|
248
|
+
// the CLI's `--model` flag only resolves the composed
|
|
249
|
+
// `<providerID>/<modelID>` form. Resuming such a session without the prefix
|
|
250
|
+
// fails with an unknown-model server error, which hits OpenRouter users
|
|
251
|
+
// hardest because their model ids carry a slash themselves.
|
|
252
|
+
await withOpenCodeHome(
|
|
253
|
+
(homeDir) => writeOpenCodeSessionDatabase(homeDir, [
|
|
254
|
+
{
|
|
255
|
+
id: 'ses_openrouter',
|
|
256
|
+
model: JSON.stringify({
|
|
257
|
+
id: 'z-ai/glm-5.3-flash',
|
|
258
|
+
providerID: 'openrouter',
|
|
259
|
+
variant: 'default',
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 'ses_qualified',
|
|
264
|
+
// An id that already carries the provider prefix must survive as-is.
|
|
265
|
+
model: JSON.stringify({
|
|
266
|
+
id: 'openrouter/z-ai/glm-5.3-flash',
|
|
267
|
+
providerID: 'openrouter',
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'ses_plain_string',
|
|
272
|
+
model: 'opencode/gpt-5.6-terra',
|
|
273
|
+
},
|
|
274
|
+
]),
|
|
275
|
+
async (adapter) => {
|
|
276
|
+
assert.equal(
|
|
277
|
+
(await adapter.getCurrentActiveModel('ses_openrouter')).model,
|
|
278
|
+
'openrouter/z-ai/glm-5.3-flash',
|
|
279
|
+
);
|
|
280
|
+
assert.equal(
|
|
281
|
+
(await adapter.getCurrentActiveModel('ses_qualified')).model,
|
|
282
|
+
'openrouter/z-ai/glm-5.3-flash',
|
|
283
|
+
);
|
|
284
|
+
assert.equal(
|
|
285
|
+
(await adapter.getCurrentActiveModel('ses_plain_string')).model,
|
|
286
|
+
'opencode/gpt-5.6-terra',
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
} finally {
|
|
291
|
+
closeConnection();
|
|
292
|
+
if (previousDatabasePath === undefined) {
|
|
293
|
+
delete process.env.DATABASE_PATH;
|
|
294
|
+
} else {
|
|
295
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
296
|
+
}
|
|
297
|
+
await rm(tempSessionsDirectory, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
@@ -151,6 +151,30 @@ const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): P
|
|
|
151
151
|
3,
|
|
152
152
|
2,
|
|
153
153
|
);
|
|
154
|
+
db.prepare(`
|
|
155
|
+
INSERT INTO session (
|
|
156
|
+
id, project_id, parent_id, slug, directory, title, version, time_created, time_updated,
|
|
157
|
+
time_archived, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
|
|
158
|
+
tokens_cache_write
|
|
159
|
+
)
|
|
160
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
161
|
+
`).run(
|
|
162
|
+
'open-child-session-1',
|
|
163
|
+
'project-1',
|
|
164
|
+
'open-session-1',
|
|
165
|
+
'open-child-session-1',
|
|
166
|
+
workspacePath,
|
|
167
|
+
'OpenCode child session',
|
|
168
|
+
'0.0.0',
|
|
169
|
+
1_700_000_002_000,
|
|
170
|
+
1_700_000_005_000,
|
|
171
|
+
null,
|
|
172
|
+
5,
|
|
173
|
+
10,
|
|
174
|
+
2,
|
|
175
|
+
1,
|
|
176
|
+
1,
|
|
177
|
+
);
|
|
154
178
|
|
|
155
179
|
const userMessageData = JSON.stringify({
|
|
156
180
|
role: 'user',
|
|
@@ -255,16 +279,29 @@ test('OpenCode session synchronizer indexes sqlite sessions without deletable tr
|
|
|
255
279
|
try {
|
|
256
280
|
await createOpenCodeDatabase(tempRoot, workspacePath);
|
|
257
281
|
await withIsolatedDatabase(() => {
|
|
282
|
+
sessionsDb.createAppSession('app-child-session-1', 'opencode', workspacePath);
|
|
283
|
+
sessionsDb.assignProviderSessionId('app-child-session-1', 'open-child-session-1');
|
|
284
|
+
|
|
258
285
|
const synchronizer = new OpenCodeSessionSynchronizer();
|
|
259
286
|
const processed = synchronizer.synchronize();
|
|
260
287
|
|
|
261
|
-
return Promise.resolve(processed).then((count) => {
|
|
288
|
+
return Promise.resolve(processed).then(async (count) => {
|
|
262
289
|
assert.equal(count, 1);
|
|
263
290
|
const indexed = sessionsDb.getSessionById('open-session-1');
|
|
264
291
|
assert.equal(indexed?.provider, 'opencode');
|
|
265
292
|
assert.equal(indexed?.project_path, workspacePath);
|
|
266
293
|
assert.equal(indexed?.custom_name, 'OpenCode indexed title');
|
|
267
294
|
assert.equal(indexed?.jsonl_path, null);
|
|
295
|
+
assert.equal(sessionsDb.getSessionById('app-child-session-1'), null);
|
|
296
|
+
assert.equal(sessionsDb.getSessionByProviderSessionId('open-child-session-1'), null);
|
|
297
|
+
|
|
298
|
+
sessionsDb.createAppSession('app-child-session-2', 'opencode', workspacePath);
|
|
299
|
+
sessionsDb.assignProviderSessionId('app-child-session-2', 'open-child-session-1');
|
|
300
|
+
await synchronizer.synchronize(new Date(1_700_000_004_500));
|
|
301
|
+
assert.equal(
|
|
302
|
+
sessionsDb.getSessionById('app-child-session-2')?.provider_session_id,
|
|
303
|
+
'open-child-session-1',
|
|
304
|
+
);
|
|
268
305
|
});
|
|
269
306
|
});
|
|
270
307
|
} finally {
|
|
@@ -290,6 +327,7 @@ test('OpenCode session synchronizer returns the app session id once provider map
|
|
|
290
327
|
assert.equal(sessionId, 'app-session-1');
|
|
291
328
|
assert.equal(sessionsDb.getAllSessions().length, 1);
|
|
292
329
|
assert.equal(sessionsDb.getSessionById('app-session-1')?.provider_session_id, 'open-session-1');
|
|
330
|
+
assert.equal(sessionsDb.getSessionById('open-child-session-1'), null);
|
|
293
331
|
});
|
|
294
332
|
});
|
|
295
333
|
} finally {
|
|
@@ -438,6 +476,7 @@ const seedOpenCodeSession = async (
|
|
|
438
476
|
CREATE TABLE session (
|
|
439
477
|
id TEXT PRIMARY KEY,
|
|
440
478
|
project_id TEXT,
|
|
479
|
+
parent_id TEXT,
|
|
441
480
|
directory TEXT,
|
|
442
481
|
title TEXT,
|
|
443
482
|
time_created INTEGER,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { CLAUDE_PREDEFINED_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
|
|
5
|
+
import { CODEX_PREDEFINED_MODELS } from '@/modules/providers/list/codex/codex-models.provider.js';
|
|
6
|
+
import { CURSOR_PREDEFINED_MODELS } from '@/modules/providers/list/cursor/cursor-models.provider.js';
|
|
7
|
+
import { OPENCODE_PREDEFINED_MODELS } from '@/modules/providers/list/opencode/opencode-models.provider.js';
|
|
8
|
+
import { providerCapabilitiesService } from '@/modules/providers/services/provider-capabilities.service.js';
|
|
9
|
+
import type { LLMProvider, ProviderModelsDefinition } from '@/shared/types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The full curated catalogs, before OpenCode narrows its list to the upstream
|
|
13
|
+
* providers a machine has connected.
|
|
14
|
+
*/
|
|
15
|
+
const CURATED_CATALOGS: Record<LLMProvider, ProviderModelsDefinition> = {
|
|
16
|
+
claude: CLAUDE_PREDEFINED_MODELS,
|
|
17
|
+
codex: CODEX_PREDEFINED_MODELS,
|
|
18
|
+
cursor: CURSOR_PREDEFINED_MODELS,
|
|
19
|
+
opencode: OPENCODE_PREDEFINED_MODELS,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('capability effort levels cover every level a curated model declares', () => {
|
|
23
|
+
// Custom models may declare only these levels, so a level added to a curated
|
|
24
|
+
// model has to be added here too, or users could not give it to their own.
|
|
25
|
+
for (const [provider, catalog] of Object.entries(CURATED_CATALOGS) as Array<[LLMProvider, ProviderModelsDefinition]>) {
|
|
26
|
+
const { effortLevels, supportsEffort } = providerCapabilitiesService.getProviderCapabilities(provider);
|
|
27
|
+
|
|
28
|
+
assert.equal(supportsEffort, effortLevels.length > 0, `${provider}: supportsEffort disagrees with effortLevels`);
|
|
29
|
+
assert.equal(new Set(effortLevels).size, effortLevels.length, `${provider}: effortLevels lists a level twice`);
|
|
30
|
+
|
|
31
|
+
const declaredLevels = new Set(
|
|
32
|
+
catalog.OPTIONS.flatMap((option) => option.effort?.values.map((level) => level.value) ?? []),
|
|
33
|
+
);
|
|
34
|
+
const missingLevels = [...declaredLevels].filter((level) => !effortLevels.includes(level));
|
|
35
|
+
assert.deepEqual(missingLevels, [], `${provider}: effortLevels misses curated levels`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -69,6 +69,7 @@ const createCatalogStore = () => {
|
|
|
69
69
|
modelId: input.id,
|
|
70
70
|
model: input.model,
|
|
71
71
|
sortOrder: readRows(provider).length,
|
|
72
|
+
effort: input.effort ?? null,
|
|
72
73
|
};
|
|
73
74
|
rows.set(provider, [...readRows(provider), record]);
|
|
74
75
|
return record;
|
|
@@ -82,7 +83,13 @@ const createCatalogStore = () => {
|
|
|
82
83
|
if (!existing) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
|
-
const updated = {
|
|
86
|
+
const updated = {
|
|
87
|
+
...existing,
|
|
88
|
+
modelId: input.id,
|
|
89
|
+
model: input.model,
|
|
90
|
+
// Mirrors the repository: an omitted effort keeps the stored levels.
|
|
91
|
+
effort: input.effort === undefined ? existing.effort : input.effort,
|
|
92
|
+
};
|
|
86
93
|
rows.set(provider, readRows(provider).map((record) => (
|
|
87
94
|
record.recordId === recordId ? updated : record
|
|
88
95
|
)));
|
|
@@ -165,6 +172,97 @@ test('custom models can be created, edited, and deleted', async () => {
|
|
|
165
172
|
assert.equal(removed.models.OPTIONS.some((option) => option.recordId === recordId), false);
|
|
166
173
|
});
|
|
167
174
|
|
|
175
|
+
test('custom models carry declared effort levels into the merged catalog', async () => {
|
|
176
|
+
const { service } = createTestService();
|
|
177
|
+
|
|
178
|
+
const created = await service.createCustomModel('claude', {
|
|
179
|
+
model: 'My Custom Model',
|
|
180
|
+
id: 'my-custom-model',
|
|
181
|
+
effort: { values: ['low', 'max'], default: 'max' },
|
|
182
|
+
});
|
|
183
|
+
const expectedEffort = { default: 'max', values: [{ value: 'low' }, { value: 'max' }] };
|
|
184
|
+
assert.deepEqual(created.model.effort, expectedEffort);
|
|
185
|
+
|
|
186
|
+
const catalog = await service.getProviderModels('claude');
|
|
187
|
+
const merged = catalog.OPTIONS.find((option) => option.value === 'my-custom-model');
|
|
188
|
+
assert.deepEqual(merged?.effort, expectedEffort);
|
|
189
|
+
|
|
190
|
+
// A rename that does not mention effort (an older client) keeps the levels.
|
|
191
|
+
const renamed = await service.updateCustomModel('claude', created.model.recordId as number, {
|
|
192
|
+
model: 'Renamed',
|
|
193
|
+
id: 'my-custom-model',
|
|
194
|
+
});
|
|
195
|
+
assert.deepEqual(renamed.model.effort, expectedEffort);
|
|
196
|
+
|
|
197
|
+
const cleared = await service.updateCustomModel('claude', created.model.recordId as number, {
|
|
198
|
+
model: 'Renamed',
|
|
199
|
+
id: 'my-custom-model',
|
|
200
|
+
effort: null,
|
|
201
|
+
});
|
|
202
|
+
assert.equal('effort' in cleared.model, false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('custom models without declared effort levels expose no effort', async () => {
|
|
206
|
+
const { service } = createTestService();
|
|
207
|
+
|
|
208
|
+
const created = await service.createCustomModel('codex', { model: 'Plain', id: 'plain-model' });
|
|
209
|
+
|
|
210
|
+
assert.equal('effort' in created.model, false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('custom effort levels must be levels the provider supports', async () => {
|
|
214
|
+
const { service } = createTestService();
|
|
215
|
+
|
|
216
|
+
await assert.rejects(
|
|
217
|
+
() => service.createCustomModel('codex', {
|
|
218
|
+
model: 'Unknown level',
|
|
219
|
+
id: 'unknown-level',
|
|
220
|
+
effort: { values: ['low', 'turbo'] },
|
|
221
|
+
}),
|
|
222
|
+
(error) => error instanceof AppError
|
|
223
|
+
&& error.code === 'INVALID_MODEL_EFFORT'
|
|
224
|
+
&& error.statusCode === 400,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
() => service.createCustomModel('cursor', {
|
|
229
|
+
model: 'Cursor effort',
|
|
230
|
+
id: 'cursor-effort',
|
|
231
|
+
effort: { values: ['low'] },
|
|
232
|
+
}),
|
|
233
|
+
(error) => error instanceof AppError
|
|
234
|
+
&& error.code === 'MODEL_EFFORT_NOT_SUPPORTED'
|
|
235
|
+
&& error.statusCode === 400,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('allowed effort levels come from the provider, not from the built-in models this machine sees', async () => {
|
|
240
|
+
// The fake adapters' built-in models declare no effort at all, which is what
|
|
241
|
+
// the real OpenCode adapter returns once it narrows its catalog to OpenCode
|
|
242
|
+
// Zen, Anthropic, or OpenAI: only its OpenCode Go models declare effort.
|
|
243
|
+
const { service } = createTestService();
|
|
244
|
+
|
|
245
|
+
const created = await service.createCustomModel('opencode', {
|
|
246
|
+
model: 'Router model',
|
|
247
|
+
id: 'openrouter/router-model',
|
|
248
|
+
effort: { values: ['none', 'high'] },
|
|
249
|
+
});
|
|
250
|
+
assert.deepEqual(created.model.effort, { values: [{ value: 'none' }, { value: 'high' }] });
|
|
251
|
+
|
|
252
|
+
// Every merged catalog carries the provider's levels for the model library,
|
|
253
|
+
// weakest first, and an empty list where effort is unsupported.
|
|
254
|
+
const expectedLevels: Record<LLMProvider, string[]> = {
|
|
255
|
+
claude: ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
|
|
256
|
+
codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
|
|
257
|
+
cursor: [],
|
|
258
|
+
opencode: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'thinking'],
|
|
259
|
+
};
|
|
260
|
+
for (const [provider, levels] of Object.entries(expectedLevels) as Array<[LLMProvider, string[]]>) {
|
|
261
|
+
assert.deepEqual((await service.getProviderModels(provider)).EFFORT_LEVELS, levels, provider);
|
|
262
|
+
}
|
|
263
|
+
assert.deepEqual(created.models.EFFORT_LEVELS, expectedLevels.opencode);
|
|
264
|
+
});
|
|
265
|
+
|
|
168
266
|
test('duplicate model ids are rejected within one provider', async () => {
|
|
169
267
|
const { service } = createTestService();
|
|
170
268
|
await service.createCustomModel('cursor', { model: 'First', id: 'custom-id' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import type { AddressInfo } from 'node:net';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -10,6 +10,9 @@ import express, { type NextFunction, type Request, type Response } from 'express
|
|
|
10
10
|
|
|
11
11
|
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
12
12
|
import providerRouter from '@/modules/providers/provider.routes.js';
|
|
13
|
+
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
14
|
+
import type { IProvider } from '@/shared/interfaces.js';
|
|
15
|
+
import type { BackgroundTaskSummary, WorkflowAgentActivity } from '@/shared/types.js';
|
|
13
16
|
import { AppError } from '@/shared/utils.js';
|
|
14
17
|
|
|
15
18
|
async function withProviderServer(
|
|
@@ -242,3 +245,229 @@ test('model routes expose immutable defaults and full custom model CRUD', async
|
|
|
242
245
|
);
|
|
243
246
|
});
|
|
244
247
|
});
|
|
248
|
+
|
|
249
|
+
test('custom model routes accept, validate, and return reasoning-effort levels', async () => {
|
|
250
|
+
await withProviderServer(async (baseUrl) => {
|
|
251
|
+
const postModel = async (provider: string, body: unknown) => {
|
|
252
|
+
const response = await fetch(`${baseUrl}/api/providers/${provider}/models`, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'content-type': 'application/json' },
|
|
255
|
+
body: JSON.stringify(body),
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
status: response.status,
|
|
259
|
+
payload: await response.json() as {
|
|
260
|
+
data?: {
|
|
261
|
+
model: { recordId: number; effort?: unknown };
|
|
262
|
+
models: { OPTIONS: Array<{ value: string; effort?: unknown }> };
|
|
263
|
+
};
|
|
264
|
+
error?: { code: string; message?: string };
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const created = await postModel('claude', {
|
|
270
|
+
model: 'My Custom Model',
|
|
271
|
+
id: 'my-custom-model',
|
|
272
|
+
effort: { values: [' low ', 'high'], default: 'high' },
|
|
273
|
+
});
|
|
274
|
+
const expectedEffort = { default: 'high', values: [{ value: 'low' }, { value: 'high' }] };
|
|
275
|
+
assert.equal(created.status, 201);
|
|
276
|
+
assert.deepEqual(created.payload.data?.model.effort, expectedEffort);
|
|
277
|
+
|
|
278
|
+
const catalogResponse = await fetch(`${baseUrl}/api/providers/claude/models`);
|
|
279
|
+
const catalogPayload = await catalogResponse.json() as {
|
|
280
|
+
data: { models: { OPTIONS: Array<{ value: string; effort?: unknown }>; EFFORT_LEVELS?: string[] } };
|
|
281
|
+
};
|
|
282
|
+
assert.deepEqual(
|
|
283
|
+
catalogPayload.data.models.OPTIONS.find((option) => option.value === 'my-custom-model')?.effort,
|
|
284
|
+
expectedEffort,
|
|
285
|
+
);
|
|
286
|
+
// The model library's toggles come from here, not from the built-in models.
|
|
287
|
+
assert.deepEqual(
|
|
288
|
+
catalogPayload.data.models.EFFORT_LEVELS,
|
|
289
|
+
['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// OpenCode's allowed levels do not depend on which upstream providers the
|
|
293
|
+
// machine running the tests has connected.
|
|
294
|
+
const openCodeModel = await postModel('opencode', {
|
|
295
|
+
model: 'Router model',
|
|
296
|
+
id: 'openrouter/router-model',
|
|
297
|
+
effort: { values: ['none', 'high'] },
|
|
298
|
+
});
|
|
299
|
+
assert.equal(openCodeModel.status, 201, JSON.stringify(openCodeModel.payload));
|
|
300
|
+
assert.deepEqual(openCodeModel.payload.data?.model.effort, { values: [{ value: 'none' }, { value: 'high' }] });
|
|
301
|
+
|
|
302
|
+
// A PATCH from a client that predates effort metadata keeps the levels.
|
|
303
|
+
const renameResponse = await fetch(
|
|
304
|
+
`${baseUrl}/api/providers/claude/models/${created.payload.data?.model.recordId}`,
|
|
305
|
+
{
|
|
306
|
+
method: 'PATCH',
|
|
307
|
+
headers: { 'content-type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({ model: 'Renamed', id: 'my-custom-model' }),
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
const renamePayload = await renameResponse.json() as { data: { model: { effort?: unknown } } };
|
|
312
|
+
assert.equal(renameResponse.status, 200);
|
|
313
|
+
assert.deepEqual(renamePayload.data.model.effort, expectedEffort);
|
|
314
|
+
|
|
315
|
+
const invalidPayloads: Array<{ provider: string; effort: unknown; code: string }> = [
|
|
316
|
+
{ provider: 'claude', effort: { values: ['low', 'low'] }, code: 'INVALID_MODEL_EFFORT' },
|
|
317
|
+
{ provider: 'claude', effort: { values: ['low', ''] }, code: 'INVALID_MODEL_EFFORT' },
|
|
318
|
+
{ provider: 'claude', effort: { values: ['low'], default: 'high' }, code: 'INVALID_MODEL_EFFORT' },
|
|
319
|
+
{ provider: 'claude', effort: { values: ['turbo'] }, code: 'INVALID_MODEL_EFFORT' },
|
|
320
|
+
{ provider: 'claude', effort: 'high', code: 'INVALID_MODEL_EFFORT' },
|
|
321
|
+
{ provider: 'cursor', effort: { values: ['low'] }, code: 'MODEL_EFFORT_NOT_SUPPORTED' },
|
|
322
|
+
];
|
|
323
|
+
for (const [index, invalid] of invalidPayloads.entries()) {
|
|
324
|
+
const rejected = await postModel(invalid.provider, {
|
|
325
|
+
model: `Invalid ${index}`,
|
|
326
|
+
id: `invalid-${index}`,
|
|
327
|
+
effort: invalid.effort,
|
|
328
|
+
});
|
|
329
|
+
assert.equal(rejected.status, 400, JSON.stringify(invalid));
|
|
330
|
+
assert.equal(rejected.payload.error?.code, invalid.code, JSON.stringify(invalid));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// An oversized list is refused by its length, before any entry is walked:
|
|
334
|
+
// the JSON parser accepts bodies of up to 50 MB.
|
|
335
|
+
const oversized = await postModel('claude', {
|
|
336
|
+
model: 'Oversized',
|
|
337
|
+
id: 'oversized',
|
|
338
|
+
effort: { values: Array.from({ length: 33 }, (_, index) => `level-${index}`) },
|
|
339
|
+
});
|
|
340
|
+
assert.equal(oversized.status, 400);
|
|
341
|
+
assert.equal(oversized.payload.error?.code, 'INVALID_MODEL_EFFORT');
|
|
342
|
+
assert.match(oversized.payload.error?.message ?? '', /at most 32 effort levels/);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('the running-sessions route reports a session held open for background work', async () => {
|
|
347
|
+
// The client polls this route to keep every tab's activity map in step, so
|
|
348
|
+
// a session whose turn ended but whose agent is still running has to be in
|
|
349
|
+
// the payload — flagged as background work, with the tasks to show and stop.
|
|
350
|
+
const tasks: BackgroundTaskSummary[] = [{
|
|
351
|
+
taskId: 'a73dc7a6442f7c415',
|
|
352
|
+
toolUseId: 'toolu_013sUz4iWcNjN6BskVkHUCAr',
|
|
353
|
+
taskType: 'local_agent',
|
|
354
|
+
description: 'Investigate the flaky test',
|
|
355
|
+
startedAt: 1_700_000_000_000,
|
|
356
|
+
}];
|
|
357
|
+
const realListProviders = providerRegistry.listProviders;
|
|
358
|
+
providerRegistry.listProviders = () => [{
|
|
359
|
+
id: 'claude',
|
|
360
|
+
runtime: { listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] },
|
|
361
|
+
} as unknown as IProvider];
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await withProviderServer(async (baseUrl) => {
|
|
365
|
+
const response = await fetch(`${baseUrl}/api/providers/sessions/running`);
|
|
366
|
+
const payload = await response.json() as { data: { sessions: unknown[] } };
|
|
367
|
+
|
|
368
|
+
assert.equal(response.status, 200);
|
|
369
|
+
assert.deepEqual(payload.data.sessions, [{
|
|
370
|
+
sessionId: 'held-session',
|
|
371
|
+
provider: 'claude',
|
|
372
|
+
startedAt: 1_700_000_000_000,
|
|
373
|
+
lastSeq: 0,
|
|
374
|
+
background: true,
|
|
375
|
+
canInterrupt: false,
|
|
376
|
+
tasks,
|
|
377
|
+
}]);
|
|
378
|
+
});
|
|
379
|
+
} finally {
|
|
380
|
+
providerRegistry.listProviders = realListProviders;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const WORKFLOW_SESSION_ID = 'claude-workflow-routes-session';
|
|
385
|
+
const WORKFLOW_RUN_ID = 'wf_16fbf852-274';
|
|
386
|
+
const FINISHED_AGENT_ID = 'aa1e064cf8bd159d6';
|
|
387
|
+
const UNREPORTED_AGENT_ID = 'ab89f2cde612a51b1';
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Writes a workflow run's directory the way a real run leaves it under the
|
|
391
|
+
* session's transcript directory: one `agent-<id>.jsonl` per agent, in the
|
|
392
|
+
* row shape an Agent subagent's transcript has, and the journal beside them.
|
|
393
|
+
*/
|
|
394
|
+
async function writeWorkflowRun(projectDirectory: string): Promise<string> {
|
|
395
|
+
const sessionPath = path.join(projectDirectory, `${WORKFLOW_SESSION_ID}.jsonl`);
|
|
396
|
+
await mkdir(projectDirectory, { recursive: true });
|
|
397
|
+
await writeFile(sessionPath, `${JSON.stringify({ type: 'user', uuid: 'u1', sessionId: WORKFLOW_SESSION_ID, timestamp: '2026-08-21T10:32:10.000Z', message: { role: 'user', content: 'audit the frontend' } })}\n`, 'utf8');
|
|
398
|
+
|
|
399
|
+
const runDir = path.join(projectDirectory, WORKFLOW_SESSION_ID, 'subagents', 'workflows', WORKFLOW_RUN_ID);
|
|
400
|
+
await mkdir(runDir, { recursive: true });
|
|
401
|
+
|
|
402
|
+
const agentRows = (agentId: string) => [
|
|
403
|
+
{ parentUuid: null, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u1`, timestamp: '2026-08-21T10:32:20.000Z', message: { role: 'user', content: 'You are the analysis step of a code-quality audit.' } },
|
|
404
|
+
{ parentUuid: `${agentId}-u1`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a1`, timestamp: '2026-08-21T10:32:25.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'tool_use', id: `toolu_${agentId}_1`, name: 'Grep', input: { pattern: 'useSidebar' } }] } },
|
|
405
|
+
{ parentUuid: `${agentId}-a1`, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u2`, timestamp: '2026-08-21T10:32:26.000Z', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: `toolu_${agentId}_1`, content: 'src/modules/sidebar/useSidebar.ts' }] } },
|
|
406
|
+
{ parentUuid: `${agentId}-u2`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a2`, timestamp: '2026-08-21T10:32:40.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'text', text: 'One hook owns the sidebar.' }] } },
|
|
407
|
+
];
|
|
408
|
+
for (const agentId of [FINISHED_AGENT_ID, UNREPORTED_AGENT_ID]) {
|
|
409
|
+
await writeFile(path.join(runDir, `agent-${agentId}.jsonl`), `${agentRows(agentId).map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
|
|
410
|
+
await writeFile(path.join(runDir, `agent-${agentId}.meta.json`), JSON.stringify({ agentType: 'workflow-subagent', spawnDepth: 1 }), 'utf8');
|
|
411
|
+
}
|
|
412
|
+
const journal = [
|
|
413
|
+
{ type: 'started', key: 'v2:one', agentId: FINISHED_AGENT_ID, label: 'audit:sidebar', phase: 'Audit' },
|
|
414
|
+
{ type: 'started', key: 'v2:two', agentId: UNREPORTED_AGENT_ID, label: 'synthesize', phase: 'Synthesize' },
|
|
415
|
+
{ type: 'result', key: 'v2:one', agentId: FINISHED_AGENT_ID, result: { area: 'sidebar' } },
|
|
416
|
+
];
|
|
417
|
+
await writeFile(path.join(runDir, 'journal.jsonl'), `${journal.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
|
|
418
|
+
|
|
419
|
+
return sessionPath;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
test('the workflow agent route reads an agent\'s timeline and status from its run directory', async () => {
|
|
423
|
+
await withProviderServer(async (baseUrl, workspacePath) => {
|
|
424
|
+
const sessionPath = await writeWorkflowRun(workspacePath);
|
|
425
|
+
const now = new Date().toISOString();
|
|
426
|
+
sessionsDb.createSession(WORKFLOW_SESSION_ID, 'claude', workspacePath, 'Workflow session', now, now, sessionPath);
|
|
427
|
+
|
|
428
|
+
const agentUrl = (runId: string, agentId: string) =>
|
|
429
|
+
`${baseUrl}/api/providers/sessions/${WORKFLOW_SESSION_ID}/workflows/${runId}/agents/${agentId}`;
|
|
430
|
+
|
|
431
|
+
// The SDK streams none of an agent's rows to the parent, so its transcript
|
|
432
|
+
// is the only record of what it did — read on demand, capped like an
|
|
433
|
+
// Agent card's, and settled by the journal beside it.
|
|
434
|
+
const finishedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, FINISHED_AGENT_ID));
|
|
435
|
+
const finished = (await finishedResponse.json() as { data: WorkflowAgentActivity }).data;
|
|
436
|
+
assert.equal(finishedResponse.status, 200);
|
|
437
|
+
assert.deepEqual(finished.agent, { id: FINISHED_AGENT_ID, label: 'audit:sidebar', model: 'claude-opus-4-1', status: 'completed' });
|
|
438
|
+
assert.equal(finished.activityCount, 2);
|
|
439
|
+
assert.deepEqual(finished.activity.map((entry) => entry.kind), ['tool', 'text']);
|
|
440
|
+
assert.equal(finished.activity[0]?.toolName, 'Grep');
|
|
441
|
+
assert.deepEqual(finished.activity[0]?.toolInput, { pattern: 'useSidebar' });
|
|
442
|
+
assert.deepEqual(finished.activity[0]?.toolResult, { content: 'src/modules/sidebar/useSidebar.ts', isError: false });
|
|
443
|
+
assert.equal(finished.activity[1]?.content, 'One hook owns the sidebar.');
|
|
444
|
+
|
|
445
|
+
// A `started` with no `result` or `failed`, and no live run of the session
|
|
446
|
+
// to still be working in: nothing can report for it any more.
|
|
447
|
+
const unreportedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, UNREPORTED_AGENT_ID));
|
|
448
|
+
const unreported = (await unreportedResponse.json() as { data: WorkflowAgentActivity }).data;
|
|
449
|
+
assert.equal(unreportedResponse.status, 200);
|
|
450
|
+
assert.equal(unreported.agent.status, 'stopped');
|
|
451
|
+
assert.equal(unreported.agent.label, 'synthesize');
|
|
452
|
+
|
|
453
|
+
// Both ids name a file under the session directory, so neither may carry
|
|
454
|
+
// anything but the harness's own shapes — not even a well-formed
|
|
455
|
+
// directory that happens to exist.
|
|
456
|
+
for (const [runId, agentId, code] of [
|
|
457
|
+
[`${WORKFLOW_RUN_ID}%2F..`, FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
|
|
458
|
+
['subagents', FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
|
|
459
|
+
[WORKFLOW_RUN_ID, '..%2Fjournal', 'INVALID_WORKFLOW_AGENT_ID'],
|
|
460
|
+
[WORKFLOW_RUN_ID, 'aa1e064cf8bd159d', 'INVALID_WORKFLOW_AGENT_ID'],
|
|
461
|
+
] as const) {
|
|
462
|
+
const response = await fetch(agentUrl(runId, agentId));
|
|
463
|
+
const payload = await response.json() as { error: { code: string } };
|
|
464
|
+
assert.equal(response.status, 400, `${runId}/${agentId}`);
|
|
465
|
+
assert.equal(payload.error.code, code);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const missingResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, 'a0000000000000000'));
|
|
469
|
+
const missing = await missingResponse.json() as { error: { code: string } };
|
|
470
|
+
assert.equal(missingResponse.status, 404);
|
|
471
|
+
assert.equal(missing.error.code, 'WORKFLOW_AGENT_NOT_FOUND');
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -141,3 +141,25 @@ test('the oldest entries are evicted over budget, but the newest survives alone'
|
|
|
141
141
|
await rm(tempDirectory, { recursive: true, force: true });
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
test('invalidating a session re-parses its unchanged transcript on the next read', async () => {
|
|
146
|
+
await withTranscriptFile(async (transcriptPath) => {
|
|
147
|
+
const cache = createSessionHistoryCache();
|
|
148
|
+
let loads = 0;
|
|
149
|
+
const loadFull = async () => {
|
|
150
|
+
loads += 1;
|
|
151
|
+
return historyResult(`load-${loads}`);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Claude's history reader decides a background agent's status from whether
|
|
155
|
+
// the session's CLI process is up — nothing the file's stat can reflect —
|
|
156
|
+
// so the runtime drops the entry when that process starts or ends.
|
|
157
|
+
const first = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
158
|
+
cache.invalidate('s1');
|
|
159
|
+
const second = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
160
|
+
|
|
161
|
+
assert.equal(loads, 2);
|
|
162
|
+
assert.notEqual(second, first);
|
|
163
|
+
assert.equal(second?.messages[0]?.content, 'load-2');
|
|
164
|
+
});
|
|
165
|
+
});
|