@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,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
|
|
8
|
+
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
9
|
+
import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
|
|
10
|
+
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
11
|
+
import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
|
|
12
|
+
|
|
13
|
+
const SESSION_ID = 'held-session';
|
|
14
|
+
|
|
15
|
+
function createFakeSocket() {
|
|
16
|
+
const socket = new EventEmitter() as EventEmitter & {
|
|
17
|
+
readyState: number;
|
|
18
|
+
frames: Array<Record<string, unknown>>;
|
|
19
|
+
send: (data: string) => void;
|
|
20
|
+
};
|
|
21
|
+
socket.readyState = 1;
|
|
22
|
+
socket.frames = [];
|
|
23
|
+
socket.send = (data: string) => socket.frames.push(JSON.parse(data) as Record<string, unknown>);
|
|
24
|
+
return socket;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type StopCall = { provider: string; sessionId: string; taskId: string };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A gateway whose runtime knows exactly one background task. There is no chat
|
|
31
|
+
* run in progress: the turn that launched the task has ended, which is the
|
|
32
|
+
* state a stop request arrives in.
|
|
33
|
+
*/
|
|
34
|
+
async function withGateway(
|
|
35
|
+
knownTaskId: string,
|
|
36
|
+
runTest: (context: {
|
|
37
|
+
socket: ReturnType<typeof createFakeSocket>;
|
|
38
|
+
stops: StopCall[];
|
|
39
|
+
}) => Promise<void>,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
42
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'chat-stop-task-'));
|
|
43
|
+
|
|
44
|
+
closeConnection();
|
|
45
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
46
|
+
await initializeDatabase();
|
|
47
|
+
|
|
48
|
+
const stops: StopCall[] = [];
|
|
49
|
+
const socket = createFakeSocket();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
sessionsDb.createAppSession(SESSION_ID, 'claude', tempDirectory);
|
|
53
|
+
|
|
54
|
+
handleChatConnection(
|
|
55
|
+
socket as never,
|
|
56
|
+
{ user: { id: 1 } } as never,
|
|
57
|
+
{
|
|
58
|
+
runtime: {
|
|
59
|
+
stopBackgroundTask: async (provider: string, sessionId: string, taskId: string) => {
|
|
60
|
+
stops.push({ provider, sessionId, taskId });
|
|
61
|
+
return taskId === knownTaskId;
|
|
62
|
+
},
|
|
63
|
+
} as never,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await runTest({ socket, stops });
|
|
68
|
+
} finally {
|
|
69
|
+
connectedClients.clear();
|
|
70
|
+
chatRunRegistry.clearAll();
|
|
71
|
+
closeConnection();
|
|
72
|
+
if (previousDatabasePath === undefined) {
|
|
73
|
+
delete process.env.DATABASE_PATH;
|
|
74
|
+
} else {
|
|
75
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
76
|
+
}
|
|
77
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The handler is async and the socket listener does not await it. */
|
|
82
|
+
const settle = () => new Promise((resolve) => { setTimeout(resolve, 30); });
|
|
83
|
+
|
|
84
|
+
test('chat.stop-task stops the task through the session\'s provider runtime', async () => {
|
|
85
|
+
await withGateway('task-1', async ({ socket, stops }) => {
|
|
86
|
+
socket.emit('message', JSON.stringify({ type: 'chat.stop-task', sessionId: SESSION_ID, taskId: 'task-1' }));
|
|
87
|
+
await settle();
|
|
88
|
+
|
|
89
|
+
// The provider comes from the session row, never from the frame.
|
|
90
|
+
assert.deepEqual(stops, [{ provider: 'claude', sessionId: SESSION_ID, taskId: 'task-1' }]);
|
|
91
|
+
// The runtime reports the stop on the session's stream; nothing is echoed here.
|
|
92
|
+
assert.deepEqual(socket.frames, []);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('a task the runtime is not tracking is refused with NO_SUCH_TASK', async () => {
|
|
97
|
+
await withGateway('task-1', async ({ socket, stops }) => {
|
|
98
|
+
socket.emit('message', JSON.stringify({ type: 'chat.stop-task', sessionId: SESSION_ID, taskId: 'already-settled' }));
|
|
99
|
+
await settle();
|
|
100
|
+
|
|
101
|
+
assert.equal(stops.length, 1);
|
|
102
|
+
assert.equal(socket.frames.length, 1);
|
|
103
|
+
assert.equal(socket.frames[0].kind, 'protocol_error');
|
|
104
|
+
assert.equal(socket.frames[0].code, 'NO_SUCH_TASK');
|
|
105
|
+
assert.equal(socket.frames[0].sessionId, SESSION_ID);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('a stop without a task id never reaches the runtime', async () => {
|
|
110
|
+
await withGateway('task-1', async ({ socket, stops }) => {
|
|
111
|
+
socket.emit('message', JSON.stringify({ type: 'chat.stop-task', sessionId: SESSION_ID }));
|
|
112
|
+
await settle();
|
|
113
|
+
|
|
114
|
+
assert.equal(stops.length, 0);
|
|
115
|
+
assert.equal(socket.frames.at(-1)?.code, 'TASK_ID_REQUIRED');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a stop for a session that does not exist is refused', async () => {
|
|
120
|
+
await withGateway('task-1', async ({ socket, stops }) => {
|
|
121
|
+
socket.emit('message', JSON.stringify({ type: 'chat.stop-task', sessionId: 'no-such-session', taskId: 'task-1' }));
|
|
122
|
+
await settle();
|
|
123
|
+
|
|
124
|
+
assert.equal(stops.length, 0);
|
|
125
|
+
assert.equal(socket.frames.at(-1)?.code, 'SESSION_NOT_FOUND');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
+
import fs from 'node:fs';
|
|
3
5
|
import os from 'node:os';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import test from 'node:test';
|
|
@@ -225,3 +227,522 @@ test('a missing project directory is reported as an error frame and starts no pt
|
|
|
225
227
|
[{ type: 'error', message: 'Invalid project path' }]
|
|
226
228
|
);
|
|
227
229
|
});
|
|
230
|
+
|
|
231
|
+
type SpawnCall = { shell: string; args: string[]; command: string; env: Record<string, string | undefined> };
|
|
232
|
+
|
|
233
|
+
function spawnRecorder(resumeSessionId: string | null = null) {
|
|
234
|
+
const calls: SpawnCall[] = [];
|
|
235
|
+
const dependencies = {
|
|
236
|
+
resolveProviderSessionId: () => resumeSessionId,
|
|
237
|
+
spawnPty: (shell: string, args: string | string[], options?: { env?: Record<string, string | undefined> }) => {
|
|
238
|
+
calls.push({
|
|
239
|
+
shell,
|
|
240
|
+
args: Array.isArray(args) ? args : [args],
|
|
241
|
+
command: Array.isArray(args) ? args[args.length - 1] : args,
|
|
242
|
+
env: options?.env ?? {},
|
|
243
|
+
});
|
|
244
|
+
return createFakePty() as never;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
return { calls, dependencies };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let launchCount = 0;
|
|
251
|
+
|
|
252
|
+
function launch(
|
|
253
|
+
dependencies: ReturnType<typeof spawnRecorder>['dependencies'],
|
|
254
|
+
init: Record<string, unknown>,
|
|
255
|
+
projectPath = process.cwd()
|
|
256
|
+
) {
|
|
257
|
+
const socket = createFakeSocket();
|
|
258
|
+
handleShellConnection(socket as never, dependencies);
|
|
259
|
+
launchCount += 1;
|
|
260
|
+
socket.emit(
|
|
261
|
+
'message',
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
type: 'init',
|
|
264
|
+
projectPath,
|
|
265
|
+
sessionId: `scheme-${launchCount}-${Date.now()}`,
|
|
266
|
+
hasSession: false,
|
|
267
|
+
...init,
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
return socket;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type ClaudeConfigFixture = {
|
|
274
|
+
configHome: string;
|
|
275
|
+
home: string;
|
|
276
|
+
projectPath: string;
|
|
277
|
+
write: (filePath: string, contents: unknown) => void;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Runs `body` against a throwaway CLAUDE_CONFIG_DIR, HOME and project folder,
|
|
282
|
+
* so the user's real Claude config and app data folder never leak into (or
|
|
283
|
+
* out of) the tests. `homeName` names the HOME folder, to test awkward paths.
|
|
284
|
+
*/
|
|
285
|
+
function withClaudeConfig(body: (fixture: ClaudeConfigFixture) => void, homeName = 'home') {
|
|
286
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shell-claude-theme-'));
|
|
287
|
+
const configHome = path.join(root, 'config');
|
|
288
|
+
const home = path.join(root, homeName);
|
|
289
|
+
const projectPath = path.join(root, 'project');
|
|
290
|
+
fs.mkdirSync(configHome, { recursive: true });
|
|
291
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
292
|
+
const savedEnv = {
|
|
293
|
+
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
|
294
|
+
HOME: process.env.HOME,
|
|
295
|
+
USERPROFILE: process.env.USERPROFILE,
|
|
296
|
+
};
|
|
297
|
+
process.env.CLAUDE_CONFIG_DIR = configHome;
|
|
298
|
+
// os.homedir() reads HOME on POSIX and USERPROFILE on Windows.
|
|
299
|
+
process.env.HOME = home;
|
|
300
|
+
process.env.USERPROFILE = home;
|
|
301
|
+
|
|
302
|
+
const write = (filePath: string, contents: unknown) => {
|
|
303
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
304
|
+
fs.writeFileSync(filePath, typeof contents === 'string' ? contents : JSON.stringify(contents));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
body({ configHome, home, projectPath, write });
|
|
309
|
+
} finally {
|
|
310
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
311
|
+
if (value === undefined) {
|
|
312
|
+
delete process.env[key];
|
|
313
|
+
} else {
|
|
314
|
+
process.env[key] = value;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The server-owned settings file a light launch points `--settings` at. The
|
|
322
|
+
// default fixture HOME has no quote in it, so bash quoting is a plain wrap.
|
|
323
|
+
const themeSettingsPath = (theme: string) =>
|
|
324
|
+
path.join(os.homedir(), '.cloudcli', `claude-shell-theme-${theme}.json`);
|
|
325
|
+
const themeFlag = (theme: string) => ` --settings '${themeSettingsPath(theme)}'`;
|
|
326
|
+
|
|
327
|
+
// The flag outranks the user's own settings, so a dark Shell tab must not send
|
|
328
|
+
// one: it would turn a daltonized or ANSI theme back into the plain dark one.
|
|
329
|
+
test('a dark Shell tab launches claude exactly as before, whatever the user theme is', () => {
|
|
330
|
+
withClaudeConfig(({ configHome, home, projectPath, write }) => {
|
|
331
|
+
write(path.join(configHome, 'settings.json'), { theme: 'dark-daltonized' });
|
|
332
|
+
const { calls, dependencies } = spawnRecorder('resumed-session-id');
|
|
333
|
+
|
|
334
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'dark' }, projectPath);
|
|
335
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'dark', bypassPermissions: true }, projectPath);
|
|
336
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'dark', hasSession: true }, projectPath);
|
|
337
|
+
launch(dependencies, { provider: 'claude' }, projectPath);
|
|
338
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'sepia' }, projectPath);
|
|
339
|
+
|
|
340
|
+
if (os.platform() !== 'win32') {
|
|
341
|
+
assert.deepEqual(
|
|
342
|
+
calls.map((call) => call.command),
|
|
343
|
+
[
|
|
344
|
+
'claude',
|
|
345
|
+
'claude --dangerously-skip-permissions',
|
|
346
|
+
'claude --resume "resumed-session-id" || claude',
|
|
347
|
+
'claude',
|
|
348
|
+
'claude',
|
|
349
|
+
]
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
assert.equal(calls[0].env.COLORFGBG, '15;0');
|
|
353
|
+
assert.equal(calls[3].env.COLORFGBG, process.env.COLORFGBG);
|
|
354
|
+
assert.equal(calls[4].env.COLORFGBG, process.env.COLORFGBG);
|
|
355
|
+
assert.equal(fs.existsSync(path.join(home, '.cloudcli')), false, 'a dark launch writes no file');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Claude Code defaults to its dark theme whatever the terminal looks like, so a
|
|
360
|
+
// light Shell tab showed dark prompt bands until the launch carried a light one.
|
|
361
|
+
test('a light Shell tab swaps a dark Claude theme for its light variant', () => {
|
|
362
|
+
const cases: Array<[string | undefined, string]> = [
|
|
363
|
+
[undefined, 'light'],
|
|
364
|
+
['dark', 'light'],
|
|
365
|
+
['dark-daltonized', 'light-daltonized'],
|
|
366
|
+
['dark-ansi', 'light-ansi'],
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
for (const [userTheme, expectedTheme] of cases) {
|
|
370
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
371
|
+
if (userTheme) {
|
|
372
|
+
write(path.join(configHome, 'settings.json'), { theme: userTheme, verbose: true });
|
|
373
|
+
}
|
|
374
|
+
const { calls, dependencies } = spawnRecorder();
|
|
375
|
+
|
|
376
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
377
|
+
|
|
378
|
+
assert.equal(calls.length, 1);
|
|
379
|
+
if (os.platform() !== 'win32') {
|
|
380
|
+
assert.equal(calls[0].command, `claude${themeFlag(expectedTheme)}`, `user theme ${userTheme}`);
|
|
381
|
+
}
|
|
382
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(themeSettingsPath(expectedTheme), 'utf8')), {
|
|
383
|
+
theme: expectedTheme,
|
|
384
|
+
});
|
|
385
|
+
assert.equal(calls[0].env.COLORFGBG, '0;15');
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('a light Shell tab keeps a light, auto or custom theme the user chose', () => {
|
|
391
|
+
for (const userTheme of ['light', 'light-daltonized', 'light-ansi', 'auto', 'custom:solarized']) {
|
|
392
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
393
|
+
write(path.join(configHome, 'settings.json'), { theme: userTheme });
|
|
394
|
+
const { calls, dependencies } = spawnRecorder();
|
|
395
|
+
|
|
396
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
397
|
+
|
|
398
|
+
assert.deepEqual(calls.map((call) => call.command), ['claude'], `user theme ${userTheme}`);
|
|
399
|
+
assert.equal(calls[0].env.COLORFGBG, '0;15');
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Mirrors resolveSetting("theme") of Claude Code 2.1.280: local, project and user
|
|
405
|
+
// settings first, then the legacy `theme` key of the global config.
|
|
406
|
+
test('the Claude theme is read in the CLI order, including the legacy global config', () => {
|
|
407
|
+
const lightLaunch = (projectPath: string) => {
|
|
408
|
+
const { calls, dependencies } = spawnRecorder();
|
|
409
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
410
|
+
return calls[0].command;
|
|
411
|
+
};
|
|
412
|
+
if (os.platform() === 'win32') {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
417
|
+
write(path.join(configHome, '.claude.json'), { theme: 'dark-daltonized' });
|
|
418
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-daltonized')}`);
|
|
419
|
+
|
|
420
|
+
write(path.join(configHome, 'settings.json'), { theme: 'sepia' });
|
|
421
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-daltonized')}`, 'invalid value is unset');
|
|
422
|
+
|
|
423
|
+
write(path.join(configHome, 'settings.json'), { theme: 'dark-ansi' });
|
|
424
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-ansi')}`, 'settings beat legacy config');
|
|
425
|
+
|
|
426
|
+
write(path.join(projectPath, '.claude', 'settings.json'), { theme: 'light' });
|
|
427
|
+
assert.equal(lightLaunch(projectPath), 'claude', 'project settings beat user settings');
|
|
428
|
+
|
|
429
|
+
write(path.join(projectPath, '.claude', 'settings.local.json'), { theme: 'dark-daltonized' });
|
|
430
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-daltonized')}`, 'local beats project');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
434
|
+
write(path.join(configHome, '.claude.json'), { theme: 'dark-daltonized' });
|
|
435
|
+
write(path.join(configHome, '.config.json'), { theme: 'dark-ansi' });
|
|
436
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-ansi')}`, 'legacy .config.json wins');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Without CLAUDE_CONFIG_DIR the CLI reads ~/.claude/settings.json and ~/.claude.json.
|
|
440
|
+
withClaudeConfig(({ projectPath, write }) => {
|
|
441
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
442
|
+
const home = process.env.HOME as string;
|
|
443
|
+
write(path.join(home, '.claude.json'), { theme: 'dark-ansi' });
|
|
444
|
+
assert.equal(lightLaunch(projectPath), `claude${themeFlag('light-ansi')}`);
|
|
445
|
+
|
|
446
|
+
write(path.join(home, '.claude', 'settings.json'), { theme: 'light-daltonized' });
|
|
447
|
+
assert.equal(lightLaunch(projectPath), 'claude');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('an unreadable or unparsable Claude config counts as unset and never blocks the launch', () => {
|
|
452
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
453
|
+
// A directory where a file is expected fails to read with EISDIR.
|
|
454
|
+
fs.mkdirSync(path.join(configHome, 'settings.json'));
|
|
455
|
+
write(path.join(configHome, '.claude.json'), '{"theme": "dark-daltonized",');
|
|
456
|
+
write(path.join(projectPath, '.claude', 'settings.local.json'), 'not json');
|
|
457
|
+
write(path.join(projectPath, '.claude', 'settings.json'), '["dark-ansi"]');
|
|
458
|
+
const { calls, dependencies } = spawnRecorder();
|
|
459
|
+
|
|
460
|
+
const socket = launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
461
|
+
|
|
462
|
+
assert.equal(calls.length, 1);
|
|
463
|
+
if (os.platform() !== 'win32') {
|
|
464
|
+
assert.equal(calls[0].command, `claude${themeFlag('light')}`);
|
|
465
|
+
}
|
|
466
|
+
const frames = socket.frames.map((frame) => JSON.parse(frame) as { type: string });
|
|
467
|
+
assert.deepEqual(frames.map((frame) => frame.type), ['output', 'claude_theme']);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// The project can be a cloned repository, and the repository decides what its
|
|
472
|
+
// `.claude/settings.local.json` is. Reading a symlink to a FIFO blocked the
|
|
473
|
+
// server until something wrote to it; one to /dev/zero exhausted its memory.
|
|
474
|
+
test('a Claude config that is not a regular file is skipped without being opened', (t) => {
|
|
475
|
+
if (os.platform() === 'win32') {
|
|
476
|
+
t.skip('needs mkfifo and /dev/null');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
withClaudeConfig(({ configHome, projectPath }) => {
|
|
480
|
+
const fifoPath = path.join(projectPath, 'settings.fifo');
|
|
481
|
+
execFileSync('mkfifo', [fifoPath]);
|
|
482
|
+
fs.mkdirSync(path.join(projectPath, '.claude'));
|
|
483
|
+
fs.symlinkSync(fifoPath, path.join(projectPath, '.claude', 'settings.local.json'));
|
|
484
|
+
fs.symlinkSync('/dev/null', path.join(projectPath, '.claude', 'settings.json'));
|
|
485
|
+
fs.symlinkSync(os.tmpdir(), path.join(configHome, 'settings.json'));
|
|
486
|
+
// A reader that opened the FIFO would get a dark-daltonized theme from this
|
|
487
|
+
// writer (and so launch light-daltonized) instead of blocking forever. A
|
|
488
|
+
// launch that skips it falls through to the default dark theme.
|
|
489
|
+
const writer = spawn('sh', ['-c', `printf %s '{"theme":"dark-daltonized"}' > "$0"`, fifoPath], {
|
|
490
|
+
stdio: 'ignore',
|
|
491
|
+
});
|
|
492
|
+
try {
|
|
493
|
+
const { calls, dependencies } = spawnRecorder();
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
|
|
496
|
+
const socket = launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
497
|
+
|
|
498
|
+
assert.ok(Date.now() - startedAt < 1000, 'the launch did not wait for the FIFO');
|
|
499
|
+
assert.deepEqual(calls.map((call) => call.command), [`claude${themeFlag('light')}`]);
|
|
500
|
+
const frames = socket.frames.map((frame) => JSON.parse(frame) as { type: string });
|
|
501
|
+
assert.deepEqual(frames.map((frame) => frame.type), ['output', 'claude_theme']);
|
|
502
|
+
} finally {
|
|
503
|
+
writer.kill();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('an oversized Claude config is skipped without being read', (t) => {
|
|
509
|
+
withClaudeConfig(({ configHome, projectPath }) => {
|
|
510
|
+
const settingsPath = path.join(configHome, 'settings.json');
|
|
511
|
+
// The CLI refuses settings files over 2 MiB. A sparse tail takes the file
|
|
512
|
+
// past that cap without using the disk.
|
|
513
|
+
fs.writeFileSync(settingsPath, '{"theme":"dark-daltonized"}');
|
|
514
|
+
fs.truncateSync(settingsPath, 2 * 1024 * 1024 + 1);
|
|
515
|
+
const readFileSync = t.mock.method(fs, 'readFileSync');
|
|
516
|
+
const { calls, dependencies } = spawnRecorder();
|
|
517
|
+
|
|
518
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
519
|
+
|
|
520
|
+
const readPaths = readFileSync.mock.calls.map((call) => String(call.arguments[0]));
|
|
521
|
+
assert.equal(readPaths.includes(settingsPath), false, 'the oversized file was read');
|
|
522
|
+
assert.equal(calls.length, 1);
|
|
523
|
+
if (os.platform() !== 'win32') {
|
|
524
|
+
assert.equal(calls[0].command, `claude${themeFlag('light')}`);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('the legacy global config may be bigger than a settings file', () => {
|
|
530
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
531
|
+
// `~/.claude.json` keeps per-project history and can grow to several MB.
|
|
532
|
+
write(path.join(configHome, '.claude.json'), {
|
|
533
|
+
theme: 'dark-daltonized',
|
|
534
|
+
padding: 'x'.repeat(3 * 1024 * 1024),
|
|
535
|
+
});
|
|
536
|
+
const { calls, dependencies } = spawnRecorder();
|
|
537
|
+
|
|
538
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
539
|
+
|
|
540
|
+
assert.equal(calls.length, 1);
|
|
541
|
+
if (os.platform() !== 'win32') {
|
|
542
|
+
assert.equal(calls[0].command, `claude${themeFlag('light-daltonized')}`);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('the light theme rides along with bypass and resume launches', () => {
|
|
548
|
+
withClaudeConfig(({ projectPath }) => {
|
|
549
|
+
const { calls, dependencies } = spawnRecorder('resumed-session-id');
|
|
550
|
+
|
|
551
|
+
launch(
|
|
552
|
+
dependencies,
|
|
553
|
+
{ hasSession: true, provider: 'claude', bypassPermissions: true, colorScheme: 'light' },
|
|
554
|
+
projectPath
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
assert.equal(calls.length, 1);
|
|
558
|
+
if (os.platform() !== 'win32') {
|
|
559
|
+
const flags = ` --dangerously-skip-permissions${themeFlag('light')}`;
|
|
560
|
+
assert.equal(calls[0].command, `claude --resume "resumed-session-id"${flags} || claude${flags}`);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// The file lives in the app's own data folder: the user's Claude config and
|
|
566
|
+
// the project are only ever read.
|
|
567
|
+
test('the light theme settings file is written once to the app data folder', () => {
|
|
568
|
+
withClaudeConfig(({ configHome, home, projectPath, write }) => {
|
|
569
|
+
write(path.join(configHome, 'settings.json'), { theme: 'dark-daltonized' });
|
|
570
|
+
const configBefore = fs.readdirSync(configHome).sort();
|
|
571
|
+
const settingsPath = path.join(home, '.cloudcli', 'claude-shell-theme-light-daltonized.json');
|
|
572
|
+
const { calls, dependencies } = spawnRecorder();
|
|
573
|
+
|
|
574
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
575
|
+
assert.equal(fs.readFileSync(settingsPath, 'utf8'), '{"theme":"light-daltonized"}\n');
|
|
576
|
+
|
|
577
|
+
// Unchanged contents are not rewritten.
|
|
578
|
+
const past = new Date('2020-01-01T00:00:00Z');
|
|
579
|
+
fs.utimesSync(settingsPath, past, past);
|
|
580
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
581
|
+
assert.equal(fs.statSync(settingsPath).mtimeMs, past.getTime());
|
|
582
|
+
|
|
583
|
+
// Stale contents are replaced.
|
|
584
|
+
fs.writeFileSync(settingsPath, '{"theme":"dark"}');
|
|
585
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
586
|
+
assert.equal(fs.readFileSync(settingsPath, 'utf8'), '{"theme":"light-daltonized"}\n');
|
|
587
|
+
|
|
588
|
+
assert.equal(calls.length, 3);
|
|
589
|
+
assert.deepEqual(fs.readdirSync(path.join(home, '.cloudcli')), ['claude-shell-theme-light-daltonized.json']);
|
|
590
|
+
assert.deepEqual(fs.readdirSync(configHome).sort(), configBefore);
|
|
591
|
+
assert.equal(fs.existsSync(path.join(projectPath, '.claude')), false);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Windows runs the command through `powershell.exe -Command`, which is why the
|
|
596
|
+
// theme travels as a file path: PowerShell strips the double quotes of inline
|
|
597
|
+
// JSON when it hands an argument to a native program.
|
|
598
|
+
test('the settings file path is quoted for bash and for PowerShell', (t) => {
|
|
599
|
+
if (os.platform() === 'win32') {
|
|
600
|
+
t.skip('asserts the bash launch and POSIX paths first');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
withClaudeConfig(({ home, projectPath }) => {
|
|
604
|
+
const settingsPath = path.join(home, '.cloudcli', 'claude-shell-theme-light.json');
|
|
605
|
+
const { calls, dependencies } = spawnRecorder('resumed-session-id');
|
|
606
|
+
|
|
607
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
608
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light', hasSession: true }, projectPath);
|
|
609
|
+
t.mock.method(os, 'platform', () => 'win32');
|
|
610
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
611
|
+
launch(dependencies, { provider: 'claude', colorScheme: 'light', hasSession: true }, projectPath);
|
|
612
|
+
|
|
613
|
+
const bashFlag = ` --settings '${settingsPath.replace(/'/g, `'\\''`)}'`;
|
|
614
|
+
const powerShellFlag = ` --settings '${settingsPath.replace(/['\u2019]/g, '$&$&')}'`;
|
|
615
|
+
assert.ok(settingsPath.includes("it's a \u2019quoted\u2019 home"), settingsPath);
|
|
616
|
+
assert.deepEqual(calls.map((call) => [call.shell, ...call.args]), [
|
|
617
|
+
['bash', '-c', `claude${bashFlag}`],
|
|
618
|
+
['bash', '-c', `claude --resume "resumed-session-id"${bashFlag} || claude${bashFlag}`],
|
|
619
|
+
['powershell.exe', '-Command', `claude${powerShellFlag}`],
|
|
620
|
+
[
|
|
621
|
+
'powershell.exe',
|
|
622
|
+
'-Command',
|
|
623
|
+
`claude --resume "resumed-session-id"${powerShellFlag}; if ($LASTEXITCODE -ne 0) { claude${powerShellFlag} }`,
|
|
624
|
+
],
|
|
625
|
+
]);
|
|
626
|
+
// Spelled out once, so the escaping above is not only checked against itself.
|
|
627
|
+
assert.ok(calls[0].command.endsWith(`/it'\\''s a \u2019quoted\u2019 home/.cloudcli/claude-shell-theme-light.json'`));
|
|
628
|
+
assert.ok(calls[2].command.endsWith(`/it''s a \u2019\u2019quoted\u2019\u2019 home/.cloudcli/claude-shell-theme-light.json'`));
|
|
629
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, 'utf8')), { theme: 'light' });
|
|
630
|
+
}, "it's a \u2019quoted\u2019 home");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('a settings file that cannot be written drops the flag, logs once and still launches', (t) => {
|
|
634
|
+
withClaudeConfig(({ home, projectPath, write }) => {
|
|
635
|
+
// A file where the app data folder should be makes every write fail.
|
|
636
|
+
write(path.join(home, '.cloudcli'), 'not a folder');
|
|
637
|
+
const warn = t.mock.method(console, 'warn', () => undefined);
|
|
638
|
+
const { calls, dependencies } = spawnRecorder();
|
|
639
|
+
|
|
640
|
+
const firstSocket = launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
641
|
+
const secondSocket = launch(dependencies, { provider: 'claude', colorScheme: 'light' }, projectPath);
|
|
642
|
+
|
|
643
|
+
assert.deepEqual(calls.map((call) => call.command), ['claude', 'claude']);
|
|
644
|
+
assert.deepEqual(calls.map((call) => call.env.COLORFGBG), ['0;15', '0;15']);
|
|
645
|
+
// The CLI paints its dark default in either app theme, so no restart hint.
|
|
646
|
+
for (const socket of [firstSocket, secondSocket]) {
|
|
647
|
+
const frames = socket.frames.map((frame) => JSON.parse(frame) as { type: string });
|
|
648
|
+
assert.deepEqual(frames.map((frame) => frame.type), ['output']);
|
|
649
|
+
}
|
|
650
|
+
assert.equal(warn.mock.callCount(), 1);
|
|
651
|
+
assert.match(String(warn.mock.calls[0].arguments[0]), /Claude theme settings file/);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// A running CLI keeps its launch-time colours. The client is told the theme the
|
|
656
|
+
// pty's claude was launched for, also when it reattaches later in another theme,
|
|
657
|
+
// so the Shell can suggest a restart when they differ.
|
|
658
|
+
test('claude launches report the app theme they started in, also on reattach', () => {
|
|
659
|
+
withClaudeConfig(({ projectPath }) => {
|
|
660
|
+
const { calls, dependencies } = spawnRecorder();
|
|
661
|
+
const themeFrames = (socket: ReturnType<typeof createFakeSocket>) =>
|
|
662
|
+
socket.frames
|
|
663
|
+
.map((frame) => JSON.parse(frame) as Record<string, unknown>)
|
|
664
|
+
.filter((frame) => frame.type === 'claude_theme');
|
|
665
|
+
const sessionId = `claude-theme-${Date.now()}`;
|
|
666
|
+
const attach = (colorScheme: string) => {
|
|
667
|
+
const socket = createFakeSocket();
|
|
668
|
+
handleShellConnection(socket as never, dependencies);
|
|
669
|
+
socket.emit(
|
|
670
|
+
'message',
|
|
671
|
+
JSON.stringify({ type: 'init', projectPath, sessionId, hasSession: false, provider: 'claude', colorScheme })
|
|
672
|
+
);
|
|
673
|
+
return socket;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const firstSocket = attach('light');
|
|
677
|
+
const reattachedSocket = attach('dark');
|
|
678
|
+
|
|
679
|
+
assert.equal(calls.length, 1, 'the second init reattached to the running pty');
|
|
680
|
+
assert.deepEqual(themeFrames(firstSocket), [{ type: 'claude_theme', colorScheme: 'light' }]);
|
|
681
|
+
assert.deepEqual(themeFrames(reattachedSocket), [{ type: 'claude_theme', colorScheme: 'light' }]);
|
|
682
|
+
|
|
683
|
+
const darkSocket = launch(dependencies, { provider: 'claude', colorScheme: 'dark' }, projectPath);
|
|
684
|
+
assert.deepEqual(themeFrames(darkSocket), [{ type: 'claude_theme', colorScheme: 'dark' }]);
|
|
685
|
+
|
|
686
|
+
const silentLaunches = [
|
|
687
|
+
launch(dependencies, { provider: 'claude' }, projectPath),
|
|
688
|
+
launch(dependencies, { provider: 'claude', initialCommand: 'claude /login', colorScheme: 'light' }, projectPath),
|
|
689
|
+
launch(dependencies, { provider: 'codex', colorScheme: 'light' }, projectPath),
|
|
690
|
+
launch(dependencies, { provider: 'plain-shell', isPlainShell: true, colorScheme: 'light' }, projectPath),
|
|
691
|
+
];
|
|
692
|
+
assert.deepEqual(silentLaunches.map(themeFrames), [[], [], [], []]);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// A restart only helps when the CLI's colours follow the app theme. A light or
|
|
697
|
+
// custom theme the user chose paints the same in either app theme.
|
|
698
|
+
test('the restart hint is only offered when a restart would change the CLI colours', () => {
|
|
699
|
+
for (const [userTheme, expectsHint] of [
|
|
700
|
+
['dark-daltonized', true],
|
|
701
|
+
['auto', true],
|
|
702
|
+
['light', false],
|
|
703
|
+
['light-daltonized', false],
|
|
704
|
+
['custom:mine', false],
|
|
705
|
+
] as const) {
|
|
706
|
+
withClaudeConfig(({ configHome, projectPath, write }) => {
|
|
707
|
+
write(path.join(configHome, 'settings.json'), { theme: userTheme });
|
|
708
|
+
const { dependencies } = spawnRecorder();
|
|
709
|
+
|
|
710
|
+
for (const colorScheme of ['light', 'dark']) {
|
|
711
|
+
const socket = launch(dependencies, { provider: 'claude', colorScheme }, projectPath);
|
|
712
|
+
const themeFrames = socket.frames
|
|
713
|
+
.map((frame) => JSON.parse(frame) as Record<string, unknown>)
|
|
714
|
+
.filter((frame) => frame.type === 'claude_theme');
|
|
715
|
+
assert.deepEqual(
|
|
716
|
+
themeFrames,
|
|
717
|
+
expectsHint ? [{ type: 'claude_theme', colorScheme }] : [],
|
|
718
|
+
`${userTheme} in a ${colorScheme} tab`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test('other shells and initial commands only get the COLORFGBG hint', () => {
|
|
726
|
+
withClaudeConfig(({ projectPath }) => {
|
|
727
|
+
const { calls, dependencies } = spawnRecorder();
|
|
728
|
+
const loginCommand = 'claude --dangerously-skip-permissions /login';
|
|
729
|
+
|
|
730
|
+
launch(
|
|
731
|
+
dependencies,
|
|
732
|
+
{ provider: 'plain-shell', isPlainShell: true, initialCommand: 'npx task-master init', colorScheme: 'light' },
|
|
733
|
+
projectPath
|
|
734
|
+
);
|
|
735
|
+
launch(dependencies, { provider: 'claude', initialCommand: loginCommand, colorScheme: 'light' }, projectPath);
|
|
736
|
+
launch(dependencies, { provider: 'codex', colorScheme: 'light' }, projectPath);
|
|
737
|
+
launch(dependencies, { provider: 'codex', colorScheme: 'dark' }, projectPath);
|
|
738
|
+
|
|
739
|
+
assert.deepEqual(
|
|
740
|
+
calls.map((call) => call.command),
|
|
741
|
+
['npx task-master init', loginCommand, 'codex', 'codex']
|
|
742
|
+
);
|
|
743
|
+
assert.deepEqual(
|
|
744
|
+
calls.map((call) => call.env.COLORFGBG),
|
|
745
|
+
['0;15', '0;15', '0;15', '15;0']
|
|
746
|
+
);
|
|
747
|
+
});
|
|
748
|
+
});
|