@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
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
2
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import type { AddressInfo } from 'node:net';
|
|
7
|
+
import os from 'node:os';
|
|
3
8
|
import path from 'node:path';
|
|
4
9
|
import { PassThrough } from 'node:stream';
|
|
5
10
|
import test from 'node:test';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { promisify } from 'node:util';
|
|
6
13
|
|
|
7
14
|
import { startCloneProject } from '@/modules/projects/services/project-clone.service.js';
|
|
8
15
|
import { AppError } from '@/shared/utils.js';
|
|
@@ -110,6 +117,66 @@ test('startCloneProject rejects github URL values that begin with option prefixe
|
|
|
110
117
|
);
|
|
111
118
|
});
|
|
112
119
|
|
|
120
|
+
for (const [label, githubUrl] of [
|
|
121
|
+
['a token as the user', 'https://ghp_supersecrettoken1234567890abcd@github.com/example/repo.git'],
|
|
122
|
+
['a user and password', 'https://user:ghp_supersecrettoken1234567890abcd@github.com/example/repo.git'],
|
|
123
|
+
['a password on an ssh URL', 'ssh://git:secret@github.com/example/repo.git'],
|
|
124
|
+
] as const) {
|
|
125
|
+
test(`startCloneProject rejects a github URL carrying ${label} before git sees it`, async () => {
|
|
126
|
+
// A credential in the URL would ride git's argv, its stderr and the
|
|
127
|
+
// clone's `.git/config` — every channel the token field keeps it out of.
|
|
128
|
+
let spawned = 0;
|
|
129
|
+
await assert.rejects(
|
|
130
|
+
async () =>
|
|
131
|
+
startCloneProject(
|
|
132
|
+
{ workspacePath: '/workspace/root', githubUrl, userId: 1 },
|
|
133
|
+
{ onProgress: () => undefined, onComplete: () => undefined },
|
|
134
|
+
buildDependencies({ spawnGitClone: () => { spawned += 1; throw new Error('must not spawn'); } }),
|
|
135
|
+
),
|
|
136
|
+
(error: unknown) => {
|
|
137
|
+
assert.ok(error instanceof AppError);
|
|
138
|
+
assert.equal(error.code, 'GITHUB_URL_CARRIES_CREDENTIALS');
|
|
139
|
+
assert.ok(!error.message.includes('ghp_supersecrettoken'), 'the error must not echo the credential');
|
|
140
|
+
return true;
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
assert.equal(spawned, 0);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test('startCloneProject rejects an http(s) URL the parser cannot read before git carries it', async () => {
|
|
148
|
+
// 'https://token@github.com:notaport/x' is no URL to the parser, so the
|
|
149
|
+
// credential check could not see the token — and git would have had it on
|
|
150
|
+
// its command line until curl refused the port.
|
|
151
|
+
let spawned = 0;
|
|
152
|
+
await assert.rejects(
|
|
153
|
+
async () =>
|
|
154
|
+
startCloneProject(
|
|
155
|
+
{ workspacePath: '/workspace/root', githubUrl: 'https://ghp_supersecrettoken1234567890abcd@github.com:notaport/example/repo.git', userId: 1 },
|
|
156
|
+
{ onProgress: () => undefined, onComplete: () => undefined },
|
|
157
|
+
buildDependencies({ spawnGitClone: () => { spawned += 1; throw new Error('must not spawn'); } }),
|
|
158
|
+
),
|
|
159
|
+
(error: unknown) => {
|
|
160
|
+
assert.ok(error instanceof AppError);
|
|
161
|
+
assert.equal(error.code, 'INVALID_GITHUB_URL');
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
assert.equal(spawned, 0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('startCloneProject accepts the ssh login name that is not a credential', async () => {
|
|
169
|
+
// `git@` on an SSH URL is the login every SSH clone uses, not a secret.
|
|
170
|
+
const gitProcess = createMockGitProcess();
|
|
171
|
+
const operation = await startCloneProject(
|
|
172
|
+
{ workspacePath: '/workspace/root', githubUrl: 'ssh://git@github.com/example/repo.git', userId: 1 },
|
|
173
|
+
{ onProgress: () => undefined, onComplete: () => undefined },
|
|
174
|
+
buildDependencies({ spawnGitClone: () => gitProcess as never }),
|
|
175
|
+
);
|
|
176
|
+
gitProcess.emit('close', 0);
|
|
177
|
+
await operation.waitForCompletion;
|
|
178
|
+
});
|
|
179
|
+
|
|
113
180
|
test('startCloneProject rejects when selected github token does not exist', async () => {
|
|
114
181
|
await assert.rejects(
|
|
115
182
|
async () =>
|
|
@@ -181,3 +248,312 @@ test('startCloneProject completes and emits complete payload when git exits succ
|
|
|
181
248
|
assert.equal(resolvedCompletePayload.message, 'Repository cloned successfully');
|
|
182
249
|
assert.equal((resolvedCompletePayload.project.projectId as string) || '', 'project-1');
|
|
183
250
|
});
|
|
251
|
+
|
|
252
|
+
type SpawnCall = { cloneUrl: string; clonePath: string; environment: NodeJS.ProcessEnv };
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Runs a clone against a mock git process and captures exactly what would be
|
|
256
|
+
* handed to `spawn`: the URL and path (the only variable argv elements) and
|
|
257
|
+
* the environment. The token has to reach git through the environment alone.
|
|
258
|
+
*/
|
|
259
|
+
async function captureSpawnCall(input: {
|
|
260
|
+
githubTokenId?: number;
|
|
261
|
+
newGithubToken?: string;
|
|
262
|
+
storedToken?: string;
|
|
263
|
+
}): Promise<SpawnCall> {
|
|
264
|
+
const gitProcess = createMockGitProcess();
|
|
265
|
+
let spawnCall: SpawnCall | null = null;
|
|
266
|
+
|
|
267
|
+
const operation = await startCloneProject(
|
|
268
|
+
{
|
|
269
|
+
workspacePath: '/workspace/root',
|
|
270
|
+
githubUrl: 'https://github.com/example/repo.git',
|
|
271
|
+
githubTokenId: input.githubTokenId,
|
|
272
|
+
newGithubToken: input.newGithubToken,
|
|
273
|
+
userId: 1,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
onProgress: () => undefined,
|
|
277
|
+
onComplete: () => undefined,
|
|
278
|
+
},
|
|
279
|
+
buildDependencies({
|
|
280
|
+
getGithubTokenById: async () => ({ github_token: input.storedToken ?? 'unused' }),
|
|
281
|
+
spawnGitClone: (cloneUrl, clonePath, environment) => {
|
|
282
|
+
spawnCall = { cloneUrl, clonePath, environment };
|
|
283
|
+
return gitProcess as any;
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
gitProcess.emit('close', 0);
|
|
289
|
+
await operation.waitForCompletion;
|
|
290
|
+
|
|
291
|
+
assert.notEqual(spawnCall, null, 'git was never spawned');
|
|
292
|
+
return spawnCall as unknown as SpawnCall;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const [label, input, token] of [
|
|
296
|
+
['a stored token', { githubTokenId: 7, storedToken: 'ghp_storedsecret1234567890' }, 'ghp_storedsecret1234567890'],
|
|
297
|
+
// Characters `url.username=` would have percent-encoded, so a check for the
|
|
298
|
+
// raw token alone could not have caught the URL channel.
|
|
299
|
+
['a new token', { newGithubToken: 'ghp_new:secret@with/odd-chars' }, 'ghp_new:secret@with/odd-chars'],
|
|
300
|
+
] as const) {
|
|
301
|
+
test(`startCloneProject hands git ${label} through the credential helper, never the clone URL`, async () => {
|
|
302
|
+
const { cloneUrl, clonePath, environment } = await captureSpawnCall(input);
|
|
303
|
+
|
|
304
|
+
// The URL git receives is the one the user typed: no username, no password,
|
|
305
|
+
// nothing encoded — so neither argv, stderr nor `.git/config` can carry it.
|
|
306
|
+
assert.equal(cloneUrl, 'https://github.com/example/repo.git');
|
|
307
|
+
assert.equal(clonePath, path.join('/workspace/root', 'repo'));
|
|
308
|
+
|
|
309
|
+
assert.equal(environment.CLOUDCLI_GITHUB_TOKEN, token);
|
|
310
|
+
assert.equal(environment.GIT_CONFIG_COUNT, '2');
|
|
311
|
+
assert.equal(environment.GIT_CONFIG_KEY_0, 'credential.helper');
|
|
312
|
+
assert.equal(environment.GIT_CONFIG_VALUE_0, '');
|
|
313
|
+
assert.equal(environment.GIT_CONFIG_KEY_1, 'credential.https://github.com.helper');
|
|
314
|
+
assert.match(environment.GIT_CONFIG_VALUE_1 ?? '', /\$CLOUDCLI_GITHUB_TOKEN/);
|
|
315
|
+
assert.equal(environment.GIT_TERMINAL_PROMPT, '0');
|
|
316
|
+
|
|
317
|
+
// The helper's command line is itself a process argv, so the token must be
|
|
318
|
+
// read from the variable rather than pasted into the helper text.
|
|
319
|
+
for (const [name, value] of Object.entries(environment)) {
|
|
320
|
+
if (name === 'CLOUDCLI_GITHUB_TOKEN') {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
assert.ok(!(value ?? '').includes(token), `token leaked into ${name}`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
test('startCloneProject leaves the credential helper unset when no token was given', async () => {
|
|
329
|
+
const { cloneUrl, environment } = await captureSpawnCall({});
|
|
330
|
+
|
|
331
|
+
assert.equal(cloneUrl, 'https://github.com/example/repo.git');
|
|
332
|
+
assert.equal(environment.GIT_TERMINAL_PROMPT, '0');
|
|
333
|
+
assert.equal(environment.GIT_CONFIG_COUNT, undefined);
|
|
334
|
+
assert.equal(environment.CLOUDCLI_GITHUB_TOKEN, undefined);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* `git clone --progress` writes every byte of its progress to stderr — stdout
|
|
339
|
+
* stays empty — so these tests drive stderr, the pipe that carries the progress
|
|
340
|
+
* the user watches and the reason a clone failed.
|
|
341
|
+
*/
|
|
342
|
+
async function runCloneWithStderr(
|
|
343
|
+
chunks: string[],
|
|
344
|
+
exitCode: number | null,
|
|
345
|
+
): Promise<{ progressMessages: string[]; failure: AppError | null }> {
|
|
346
|
+
const gitProcess = createMockGitProcess();
|
|
347
|
+
const progressMessages: string[] = [];
|
|
348
|
+
|
|
349
|
+
const operation = await startCloneProject(
|
|
350
|
+
{
|
|
351
|
+
workspacePath: '/workspace/root',
|
|
352
|
+
githubUrl: 'https://github.com/example/repo.git',
|
|
353
|
+
newGithubToken: 'ghp_supersecrettoken1234567890abcd',
|
|
354
|
+
userId: 1,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
onProgress: (message) => {
|
|
358
|
+
progressMessages.push(message);
|
|
359
|
+
},
|
|
360
|
+
onComplete: () => undefined,
|
|
361
|
+
},
|
|
362
|
+
buildDependencies({ spawnGitClone: () => gitProcess as any }),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
for (const chunk of chunks) {
|
|
366
|
+
gitProcess.stderr.write(chunk);
|
|
367
|
+
}
|
|
368
|
+
gitProcess.stderr.end();
|
|
369
|
+
// Let the PassThrough deliver every `data` event and its `end` before the
|
|
370
|
+
// process is reported closed, exactly as a real pipe does.
|
|
371
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
372
|
+
gitProcess.emit('close', exitCode);
|
|
373
|
+
|
|
374
|
+
let failure: AppError | null = null;
|
|
375
|
+
try {
|
|
376
|
+
await operation.waitForCompletion;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
failure = error as AppError;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { progressMessages, failure };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
test('startCloneProject forwards each stderr chunk to onProgress as it arrives', async () => {
|
|
385
|
+
const gitProcess = createMockGitProcess();
|
|
386
|
+
const progressMessages: string[] = [];
|
|
387
|
+
|
|
388
|
+
const operation = await startCloneProject(
|
|
389
|
+
{
|
|
390
|
+
workspacePath: '/workspace/root',
|
|
391
|
+
githubUrl: 'https://github.com/example/repo.git',
|
|
392
|
+
newGithubToken: 'ghp_supersecrettoken1234567890abcd',
|
|
393
|
+
userId: 1,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
onProgress: (message) => {
|
|
397
|
+
progressMessages.push(message);
|
|
398
|
+
},
|
|
399
|
+
onComplete: () => undefined,
|
|
400
|
+
},
|
|
401
|
+
buildDependencies({ spawnGitClone: () => gitProcess as any }),
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
assert.deepEqual(progressMessages, ["Cloning into 'repo'..."]);
|
|
405
|
+
|
|
406
|
+
// Each chunk has to surface on its own event before the next one is written
|
|
407
|
+
// and long before the process exits: the progress UI is fed by these events,
|
|
408
|
+
// so anything buffered until close would leave the user staring at nothing.
|
|
409
|
+
const chunks = [
|
|
410
|
+
'remote: Enumerating objects: 20, done.\n',
|
|
411
|
+
'Receiving objects: 45% (9/20)\r',
|
|
412
|
+
'Receiving objects: 100% (20/20), done.\n',
|
|
413
|
+
];
|
|
414
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
415
|
+
gitProcess.stderr.write(chunk);
|
|
416
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
417
|
+
assert.deepEqual(
|
|
418
|
+
progressMessages,
|
|
419
|
+
["Cloning into 'repo'...", ...chunks.slice(0, index + 1).map((line) => line.trim())],
|
|
420
|
+
`chunk ${index + 1} did not arrive on its own event`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
gitProcess.stderr.end();
|
|
425
|
+
gitProcess.emit('close', 0);
|
|
426
|
+
await operation.waitForCompletion;
|
|
427
|
+
|
|
428
|
+
assert.deepEqual(progressMessages, [
|
|
429
|
+
"Cloning into 'repo'...",
|
|
430
|
+
'remote: Enumerating objects: 20, done.',
|
|
431
|
+
'Receiving objects: 45% (9/20)',
|
|
432
|
+
'Receiving objects: 100% (20/20), done.',
|
|
433
|
+
]);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('startCloneProject reports the git failure reason from stderr', async () => {
|
|
437
|
+
const { failure } = await runCloneWithStderr(
|
|
438
|
+
["remote: Repository not found.\nfatal: repository 'https://github.com/example/repo.git/' not found\n"],
|
|
439
|
+
128,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
assert.equal(failure?.code, 'GIT_CLONE_FAILED');
|
|
443
|
+
assert.equal(failure?.message, 'Repository not found. Please check the URL and ensure you have access.');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('startCloneProject falls back to the last stderr text for an unrecognised failure', async () => {
|
|
447
|
+
const { failure } = await runCloneWithStderr(
|
|
448
|
+
["fatal: unable to access 'https://github.com/example/repo.git/': SSL error\n"],
|
|
449
|
+
128,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
assert.equal(failure?.code, 'GIT_CLONE_FAILED');
|
|
453
|
+
assert.equal(failure?.message, "fatal: unable to access 'https://github.com/example/repo.git/': SSL error");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const execFileAsync = promisify(execFile);
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Real git, real clone: proves the credential-helper environment is something
|
|
460
|
+
* git accepts, and that the resulting repository records a credential-free
|
|
461
|
+
* remote. The `file://` transport never asks the helper for anything, so a
|
|
462
|
+
* placeholder token stands in for the real one.
|
|
463
|
+
*/
|
|
464
|
+
test('startCloneProject clones a real repository with the credential helper in git environment', async () => {
|
|
465
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'project-clone-service-'));
|
|
466
|
+
try {
|
|
467
|
+
const seedPath = path.join(root, 'seed');
|
|
468
|
+
const originPath = path.join(root, 'origin.git');
|
|
469
|
+
const workspacePath = path.join(root, 'workspace');
|
|
470
|
+
await execFileAsync('git', ['init', '-q', seedPath]);
|
|
471
|
+
await execFileAsync('git', [
|
|
472
|
+
'-C', seedPath,
|
|
473
|
+
'-c', 'user.email=clone@example.test',
|
|
474
|
+
'-c', 'user.name=Clone Test',
|
|
475
|
+
'commit', '-q', '--allow-empty', '-m', 'initial',
|
|
476
|
+
]);
|
|
477
|
+
await execFileAsync('git', ['clone', '-q', '--bare', seedPath, originPath]);
|
|
478
|
+
|
|
479
|
+
const originUrl = pathToFileURL(originPath).href;
|
|
480
|
+
const token = 'ghp_placeholder-file-transport-never-asks';
|
|
481
|
+
const progressMessages: string[] = [];
|
|
482
|
+
let completeMessage = '';
|
|
483
|
+
|
|
484
|
+
const operation = await startCloneProject(
|
|
485
|
+
{
|
|
486
|
+
workspacePath,
|
|
487
|
+
githubUrl: originUrl,
|
|
488
|
+
newGithubToken: token,
|
|
489
|
+
userId: 1,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
onProgress: (message) => {
|
|
493
|
+
progressMessages.push(message);
|
|
494
|
+
},
|
|
495
|
+
onComplete: ({ message }) => {
|
|
496
|
+
completeMessage = message;
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
validatePath: async () => ({ valid: true, resolvedPath: workspacePath }),
|
|
501
|
+
registerProject: async (projectPath, customName) => ({
|
|
502
|
+
project: { path: projectPath, name: customName },
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
await operation.waitForCompletion;
|
|
507
|
+
|
|
508
|
+
assert.equal(completeMessage, 'Repository cloned successfully');
|
|
509
|
+
// The first message is synthesised by the service; everything after it
|
|
510
|
+
// came off git's own stderr, which is what the progress UI relies on.
|
|
511
|
+
assert.ok(
|
|
512
|
+
progressMessages.length >= 2,
|
|
513
|
+
`no progress arrived from git: ${JSON.stringify(progressMessages)}`,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const cloneConfig = await readFile(path.join(workspacePath, 'origin', '.git', 'config'), 'utf8');
|
|
517
|
+
assert.match(cloneConfig, new RegExp(`url = ${originUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
518
|
+
assert.ok(!cloneConfig.includes(token), `token written to .git/config: ${cloneConfig}`);
|
|
519
|
+
} finally {
|
|
520
|
+
await rm(root, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('startCloneProject offers the GitHub token to no host but github.com', async () => {
|
|
525
|
+
// The helper used to answer every credential challenge: a clone from any
|
|
526
|
+
// host the user typed — over plain http too — received the GitHub token.
|
|
527
|
+
// Real git against a local server that challenges for Basic auth.
|
|
528
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'project-clone-scope-'));
|
|
529
|
+
const authorizations: Array<string | null> = [];
|
|
530
|
+
const server = http.createServer((request, response) => {
|
|
531
|
+
authorizations.push(request.headers.authorization ?? null);
|
|
532
|
+
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="probe"' });
|
|
533
|
+
response.end('auth required');
|
|
534
|
+
});
|
|
535
|
+
await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve); });
|
|
536
|
+
const { port } = server.address() as AddressInfo;
|
|
537
|
+
const token = 'ghp_supersecrettoken1234567890abcd';
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
let failure = '';
|
|
541
|
+
const operation = await startCloneProject(
|
|
542
|
+
{ workspacePath: root, githubUrl: `http://127.0.0.1:${port}/example/repo.git`, newGithubToken: token, userId: 1 },
|
|
543
|
+
{ onProgress: () => undefined, onComplete: () => undefined },
|
|
544
|
+
{ validatePath: async () => ({ valid: true, resolvedPath: root }) },
|
|
545
|
+
);
|
|
546
|
+
await operation.waitForCompletion.catch((error: unknown) => { failure = error instanceof Error ? error.message : String(error); });
|
|
547
|
+
|
|
548
|
+
assert.ok(authorizations.length >= 1, 'git never reached the server');
|
|
549
|
+
assert.ok(
|
|
550
|
+
authorizations.every((authorization) => authorization === null),
|
|
551
|
+
`the token was offered to a foreign host: ${JSON.stringify(authorizations)}`,
|
|
552
|
+
);
|
|
553
|
+
assert.match(failure, /Authentication failed/);
|
|
554
|
+
} finally {
|
|
555
|
+
server.close();
|
|
556
|
+
await rm(root, { recursive: true, force: true });
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
@@ -6,7 +6,7 @@ import spawn from 'cross-spawn';
|
|
|
6
6
|
|
|
7
7
|
import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
|
|
8
8
|
import type { IProviderAuth } from '@/shared/interfaces.js';
|
|
9
|
-
import type { ProviderAuthStatus } from '@/shared/types.js';
|
|
9
|
+
import type { ProviderAuthStatus, ProviderAuthSubscriptionOverride } from '@/shared/types.js';
|
|
10
10
|
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
|
11
11
|
|
|
12
12
|
type ClaudeCredentialsStatus = {
|
|
@@ -14,6 +14,16 @@ type ClaudeCredentialsStatus = {
|
|
|
14
14
|
email: string | null;
|
|
15
15
|
method: string | null;
|
|
16
16
|
error?: string;
|
|
17
|
+
subscriptionOverride?: ProviderAuthSubscriptionOverride;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An API-key style credential found in the env or settings.json, plus the
|
|
22
|
+
* user-facing label the settings UI has always shown for it. The labels are
|
|
23
|
+
* kept verbatim so existing consumers of `email` see no change.
|
|
24
|
+
*/
|
|
25
|
+
type ClaudeApiKeyCredential = Pick<ProviderAuthSubscriptionOverride, 'variable' | 'source'> & {
|
|
26
|
+
label: string;
|
|
17
27
|
};
|
|
18
28
|
|
|
19
29
|
const hasErrorCode = (error: unknown, code: string): boolean => (
|
|
@@ -62,6 +72,7 @@ export class ClaudeProviderAuth implements IProviderAuth {
|
|
|
62
72
|
email: credentials.authenticated ? credentials.email || 'Authenticated' : credentials.email,
|
|
63
73
|
method: credentials.method,
|
|
64
74
|
error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
|
|
75
|
+
...(credentials.subscriptionOverride ? { subscriptionOverride: credentials.subscriptionOverride } : {}),
|
|
65
76
|
};
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -80,26 +91,54 @@ export class ClaudeProviderAuth implements IProviderAuth {
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
83
|
-
*
|
|
94
|
+
* Finds an API-key style credential in the same order Claude Code checks them:
|
|
95
|
+
* process env first (auth token, then API key), then the settings.json env block.
|
|
84
96
|
*/
|
|
85
|
-
private
|
|
86
|
-
const missingCredentialsError = 'Claude CLI is not authenticated. Run claude /login or configure ANTHROPIC_API_KEY.';
|
|
87
|
-
|
|
97
|
+
private findApiKeyCredential(settingsEnv: Record<string, unknown>): ClaudeApiKeyCredential | null {
|
|
88
98
|
if (process.env.ANTHROPIC_AUTH_TOKEN?.trim()) {
|
|
89
|
-
return {
|
|
99
|
+
return { variable: 'ANTHROPIC_AUTH_TOKEN', source: 'process_env', label: 'Auth Token' };
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
if (process.env.ANTHROPIC_API_KEY?.trim()) {
|
|
93
|
-
return {
|
|
103
|
+
return { variable: 'ANTHROPIC_API_KEY', source: 'process_env', label: 'API Key Auth' };
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
const settingsEnv = await this.loadSettingsEnv();
|
|
97
106
|
if (readOptionalString(settingsEnv.ANTHROPIC_API_KEY)) {
|
|
98
|
-
return {
|
|
107
|
+
return { variable: 'ANTHROPIC_API_KEY', source: 'settings_file', label: 'API Key Auth' };
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
if (readOptionalString(settingsEnv.ANTHROPIC_AUTH_TOKEN)) {
|
|
102
|
-
return {
|
|
111
|
+
return { variable: 'ANTHROPIC_AUTH_TOKEN', source: 'settings_file', label: 'Configured via settings.json' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Checks Claude credentials in the same priority order used by Claude Code.
|
|
119
|
+
*/
|
|
120
|
+
private async checkCredentials(): Promise<ClaudeCredentialsStatus> {
|
|
121
|
+
const settingsEnv = await this.loadSettingsEnv();
|
|
122
|
+
|
|
123
|
+
const apiKey = this.findApiKeyCredential(settingsEnv);
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
const status: ClaudeCredentialsStatus = { authenticated: true, email: apiKey.label, method: 'api_key' };
|
|
126
|
+
|
|
127
|
+
// The key wins, but Claude Code says nothing when it does so while a
|
|
128
|
+
// `claude /login` subscription is also signed in — every request is then
|
|
129
|
+
// billed pay-as-you-go to the key (issue #568). Surface the bypassed
|
|
130
|
+
// login so the settings page can warn about it; an expired or missing
|
|
131
|
+
// login is not being bypassed, so it stays silent.
|
|
132
|
+
const subscription = await this.readCredentialsFile();
|
|
133
|
+
if (subscription.authenticated) {
|
|
134
|
+
status.subscriptionOverride = {
|
|
135
|
+
variable: apiKey.variable,
|
|
136
|
+
source: apiKey.source,
|
|
137
|
+
subscriptionEmail: subscription.email,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return status;
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim()) {
|
|
@@ -110,6 +149,17 @@ export class ClaudeProviderAuth implements IProviderAuth {
|
|
|
110
149
|
return { authenticated: true, email: 'OAuth Token (long-lived)', method: 'environment' };
|
|
111
150
|
}
|
|
112
151
|
|
|
152
|
+
return this.readCredentialsFile();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reads the `claude /login` OAuth session from ~/.claude/.credentials.json and
|
|
157
|
+
* reports whether it is still usable. Shared by the fallback path (when no
|
|
158
|
+
* env credential exists) and by the API-key path (to detect a bypassed login).
|
|
159
|
+
*/
|
|
160
|
+
private async readCredentialsFile(): Promise<ClaudeCredentialsStatus> {
|
|
161
|
+
const missingCredentialsError = 'Claude CLI is not authenticated. Run claude /login or configure ANTHROPIC_API_KEY.';
|
|
162
|
+
|
|
113
163
|
try {
|
|
114
164
|
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
115
165
|
const content = await readFile(credPath, 'utf8');
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
ProviderModelOption,
|
|
8
8
|
ProviderModelsDefinition,
|
|
9
9
|
} from '@/shared/types.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildDefaultProviderCurrentActiveModel,
|
|
12
|
+
isPlaceholderProviderModel,
|
|
13
|
+
stripAnsiSequences,
|
|
14
|
+
} from '@/shared/utils.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Ultracode is not one of the SDK's reasoning-effort levels. Selecting it runs the turn at
|
|
@@ -179,20 +183,6 @@ type ClaudeInitEvent = {
|
|
|
179
183
|
};
|
|
180
184
|
};
|
|
181
185
|
|
|
182
|
-
const ANSI_PATTERN = new RegExp(
|
|
183
|
-
'[\\u001B\\u009B][[\\]()#;?]*(?:'
|
|
184
|
-
+ '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
|
|
185
|
-
+ '|(?:[\\dA-PR-TZcf-ntqry=><~]))',
|
|
186
|
-
'g',
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Claude Code stamps locally-synthesized rows (API-error placeholders and the
|
|
191
|
-
* like) with `model: "<synthetic>"`. Angle-bracketed values are placeholders,
|
|
192
|
-
* never real model ids, and must not be surfaced as the session's model.
|
|
193
|
-
*/
|
|
194
|
-
const isPlaceholderModel = (model: string): boolean => model.startsWith('<') && model.endsWith('>');
|
|
195
|
-
|
|
196
186
|
/** Exported for tests. */
|
|
197
187
|
export const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: string): string | null => {
|
|
198
188
|
const eventSessionId = event.sessionId ?? event.session_id;
|
|
@@ -206,16 +196,14 @@ export const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: strin
|
|
|
206
196
|
}
|
|
207
197
|
|
|
208
198
|
const directModel = event.model?.trim();
|
|
209
|
-
if (directModel && !
|
|
199
|
+
if (directModel && !isPlaceholderProviderModel(directModel)) {
|
|
210
200
|
return directModel;
|
|
211
201
|
}
|
|
212
202
|
|
|
213
203
|
const messageModel = event.message?.model?.trim();
|
|
214
|
-
return messageModel && !
|
|
204
|
+
return messageModel && !isPlaceholderProviderModel(messageModel) ? messageModel : null;
|
|
215
205
|
};
|
|
216
206
|
|
|
217
|
-
const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, '');
|
|
218
|
-
|
|
219
207
|
const extractTaggedContent = (content: string, tagName: string): string | null => {
|
|
220
208
|
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
221
209
|
const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
|
|
@@ -225,17 +213,17 @@ const extractTaggedContent = (content: string, tagName: string): string | null =
|
|
|
225
213
|
const extractClaudeModelFromTextContent = (content: string): string | null => {
|
|
226
214
|
const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
|
|
227
215
|
if (localCommandStdout !== null) {
|
|
228
|
-
const cleanedStdout =
|
|
216
|
+
const cleanedStdout = stripAnsiSequences(localCommandStdout).replace(/\s+/g, ' ').trim();
|
|
229
217
|
const changedModel = /(?:set|changed|switched)\s+model\s+to\s+(.+?)\.?$/i.exec(cleanedStdout);
|
|
230
218
|
const stdoutModel = changedModel?.[1]?.trim();
|
|
231
219
|
// A placeholder stdout hit must not shadow a real <model> tag further down.
|
|
232
|
-
if (stdoutModel && !
|
|
220
|
+
if (stdoutModel && !isPlaceholderProviderModel(stdoutModel)) {
|
|
233
221
|
return stdoutModel;
|
|
234
222
|
}
|
|
235
223
|
}
|
|
236
224
|
|
|
237
225
|
const modelTag = extractTaggedContent(content, 'model')?.trim();
|
|
238
|
-
return modelTag && !
|
|
226
|
+
return modelTag && !isPlaceholderProviderModel(modelTag) ? modelTag : null;
|
|
239
227
|
};
|
|
240
228
|
|
|
241
229
|
const extractClaudeModelFromMessageContent = (content: unknown): string | null => {
|