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