@cloudcli-ai/cloudcli 1.37.1 → 1.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/abnfDiagram-N423BO3Z-CJntskwA.js +1 -0
- package/dist/assets/arc-CIfH4uMN.js +1 -0
- package/dist/assets/architectureDiagram-T3A2C74G-_IpEkukg.js +36 -0
- package/dist/assets/blockDiagram-VBNYF7ZC-BttxrpgZ.js +132 -0
- package/dist/assets/c4Diagram-5PPSVZJV-g51dSRr1.js +10 -0
- package/dist/assets/channel-C5xU9jMw.js +1 -0
- package/dist/assets/chunk-2GRJ4B5K-D_-LbQUa.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BtlHzwaO.js +1 -0
- package/dist/assets/chunk-5RXB4S5H-UzkpvpAT.js +231 -0
- package/dist/assets/chunk-5VM5RSS4-BKwx5Gag.js +15 -0
- package/dist/assets/chunk-6Q2QTUOP-D_KEBH5f.js +88 -0
- package/dist/assets/chunk-GF5L2VYU-rK2DGyto.js +206 -0
- package/dist/assets/chunk-JWPE2WC7-CeBl_N9i.js +1 -0
- package/dist/assets/chunk-KBJHAD2P-BiQLWVIr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-C1knVGV5.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-CtKbJkvF.js +1 -0
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-DjX8y40Z.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DM6YeRba.js +178 -0
- package/dist/assets/cynefinDiagram-MW4NZA55-wZJ-vy5B.js +62 -0
- package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
- package/dist/assets/dagre-VZM6K2ZE-BzP0ejkH.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-7IWD3JNH-BTL_j23J.js +30 -0
- package/dist/assets/diagram-B4RE2ZJO-Cf1Norgu.js +3 -0
- package/dist/assets/diagram-LBJQPF4R-DPiUEKr-.js +24 -0
- package/dist/assets/diagram-Q27KOJAE-CTcTpiz5.js +24 -0
- package/dist/assets/diagram-UB23O5K3-DPMSdYm0.js +41 -0
- package/dist/assets/ebnfDiagram-BXEA7PRR-7yvD6uiF.js +1 -0
- package/dist/assets/erDiagram-JOGREHBK-CfgeDoV6.js +85 -0
- package/dist/assets/flowDiagram-UKHOOZJN-B27ng5zu.js +156 -0
- package/dist/assets/ganttDiagram-PKOTCBZU-CcC6b_7t.js +292 -0
- package/dist/assets/gitGraphDiagram-DS77QQ5N-BX5kPGrO.js +106 -0
- package/dist/assets/graph-Cx-EqisW.js +1 -0
- package/dist/assets/index-BTDHQ6Id.css +32 -0
- package/dist/assets/index-sFfz8iBR.js +4931 -0
- package/dist/assets/infoDiagram-6WML65LV-CUP-SlN4.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-WSZJBQD7-BTTDhOpX.js +70 -0
- package/dist/assets/journeyDiagram-NVQOT4AX-ARXcc61m.js +139 -0
- package/dist/assets/kanban-definition-27J2QSJJ-BieMpqMA.js +89 -0
- package/dist/assets/layout-D6BstSzF.js +1 -0
- package/dist/assets/linear-61vxjRnP.js +1 -0
- package/dist/assets/map-CV9uif0g.js +1 -0
- package/dist/assets/mermaid.core-nhnwkfQx.js +312 -0
- package/dist/assets/mindmap-definition-FAOFIHXS-Cm6x5vT1.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-VL7TDLO6-CcYBq_9H.js +1 -0
- package/dist/assets/pieDiagram-7S7Q4E2Y-CfvIjfzo.js +39 -0
- package/dist/assets/quadrantDiagram-CIZ2JOQS-SgYoWPNb.js +7 -0
- package/dist/assets/railroadDiagram-AXF67PYL-D2Mu5E_f.js +1 -0
- package/dist/assets/requirementDiagram-LRYGKXZP-Azwe_k4Q.js +84 -0
- package/dist/assets/sankeyDiagram-W5VNT64P-DN1Ju3PA.js +40 -0
- package/dist/assets/sequenceDiagram-SI44F4Z6-Bj4qC34S.js +162 -0
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-Co4PdklG.js +1 -0
- package/dist/assets/stateDiagram-OKZ733FA-nOvL9dRC.js +1 -0
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
- package/dist/assets/swimlanes-SLNWSIFB-CCK3wMid.js +2 -0
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
- package/dist/assets/timeline-definition-Z64GVDOM-BC29RNFm.js +120 -0
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/vennDiagram-T6HMQDX7-pZlZCtEo.js +34 -0
- package/dist/assets/wardleyDiagram-T6FBY63Y-De8k2-zM.js +78 -0
- package/dist/assets/xychartDiagram-ELKLHX3M-Bk8ykH0w.js +7 -0
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +8 -5
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +2 -1
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +2 -2
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/auth/auth.middleware.js +1 -1
- package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
- package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/browser-use/index.js +2 -0
- package/dist-server/server/modules/browser-use/index.js.map +1 -1
- package/dist-server/server/modules/cli/cli.js +9 -3
- package/dist-server/server/modules/cli/cli.js.map +1 -1
- package/dist-server/server/modules/cli/cli.module.js +0 -1
- package/dist-server/server/modules/cli/cli.module.js.map +1 -1
- package/dist-server/server/modules/cli/cli.service.js +0 -18
- package/dist-server/server/modules/cli/cli.service.js.map +1 -1
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
- package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
- package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +1 -1
- package/dist-server/server/modules/database/connection.js.map +1 -1
- package/dist-server/server/modules/database/index.js +5 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +32 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
- package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +92 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/file-tree/file-tree.service.js +18 -10
- 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 +62 -11
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch.service.js +15 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
- package/dist-server/server/modules/git/git.routes.js +15 -7
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
- package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
- package/dist-server/server/modules/notifications/index.js +3 -1
- package/dist-server/server/modules/notifications/index.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +422 -86
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +29 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +63 -7
- 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 +87 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +35 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/index.js +5 -0
- package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
- package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
- package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
- package/dist-server/server/modules/user/user.module.js +10 -1
- package/dist-server/server/modules/user/user.module.js.map +1 -1
- package/dist-server/server/modules/user/user.routes.js +44 -0
- package/dist-server/server/modules/user/user.routes.js.map +1 -1
- package/dist-server/server/modules/user/user.service.js +75 -0
- package/dist-server/server/modules/user/user.service.js.map +1 -1
- package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
- package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +7 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +22 -5
- package/dist-server/server/shared/claude-cli-path.js.map +1 -1
- package/dist-server/server/shared/index.js +3 -0
- package/dist-server/server/shared/index.js.map +1 -0
- package/dist-server/server/shared/message-unification.js +376 -0
- package/dist-server/server/shared/message-unification.js.map +1 -0
- package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
- package/dist-server/server/shared/tests/message-unification.test.js +174 -0
- package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +42 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +23 -16
- package/scripts/promote-dist-server.mjs +50 -0
- package/server/index.ts +12 -6
- package/server/modules/agent/agent.module.ts +2 -1
- package/server/modules/agent/agent.routes.ts +2 -2
- package/server/modules/agent/tests/agent.routes.test.ts +33 -1
- package/server/modules/auth/auth.middleware.ts +2 -2
- package/server/modules/browser-use/browser-use-runtime.ts +10 -0
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/browser-use/browser-use.service.ts +5 -8
- package/server/modules/browser-use/index.ts +3 -0
- package/server/modules/cli/cli.module.ts +0 -1
- package/server/modules/cli/cli.service.ts +0 -20
- package/server/modules/cli/cli.ts +9 -3
- package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
- package/server/modules/cli/tests/cli.service.test.ts +0 -3
- package/server/modules/database/connection.ts +2 -1
- package/server/modules/database/index.ts +10 -0
- package/server/modules/database/migrations.ts +37 -0
- package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
- package/server/modules/database/repositories/session-drafts.db.ts +171 -0
- package/server/modules/database/repositories/sessions.db.ts +196 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +96 -0
- package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
- package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
- package/server/modules/file-tree/file-tree.service.ts +21 -10
- package/server/modules/file-tree/tests/file-tree.service.test.ts +65 -11
- package/server/modules/git/git-branch.service.ts +43 -0
- package/server/modules/git/git.routes.ts +16 -9
- package/server/modules/git/tests/git-branch.service.test.ts +60 -0
- package/server/modules/git/tests/git-init.routes.test.ts +83 -0
- package/server/modules/notifications/index.ts +2 -0
- package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
- package/server/modules/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
- package/server/modules/providers/list/claude/claude-runtime.provider.js +463 -93
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
- package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
- package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
- package/server/modules/providers/provider.routes.ts +36 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-conversations-search.service.ts +94 -7
- package/server/modules/providers/services/session-history-cache.service.ts +139 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
- package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
- package/server/modules/providers/services/sessions.service.ts +201 -9
- package/server/modules/providers/tests/claude-auth.test.ts +58 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
- package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
- package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
- package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
- package/server/modules/providers/tests/opencode-models.test.ts +166 -8
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/provider.routes.test.ts +61 -0
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
- package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
- package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
- package/server/modules/providers/tests/sessions.service.test.ts +67 -1
- package/server/modules/providers/tests/skills.test.ts +21 -5
- package/server/modules/scheduled-messages/index.ts +8 -0
- package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
- package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
- package/server/modules/user/tests/user.service.test.ts +90 -0
- package/server/modules/user/user.module.ts +10 -1
- package/server/modules/user/user.routes.ts +44 -0
- package/server/modules/user/user.service.ts +105 -0
- package/server/modules/voice/tests/voice.service.test.ts +3 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
- package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +62 -0
- package/server/shared/message-unification.ts +449 -0
- package/server/shared/tests/claude-cli-path.test.ts +58 -1
- package/server/shared/tests/message-unification.test.ts +218 -0
- package/server/shared/types.ts +123 -3
- package/server/shared/utils.ts +50 -0
- package/server/tsconfig.json +6 -2
- package/README.de.md +0 -257
- package/README.ja.md +0 -249
- package/README.ko.md +0 -249
- package/README.ru.md +0 -257
- package/README.tr.md +0 -258
- package/README.zh-CN.md +0 -249
- package/README.zh-TW.md +0 -249
- package/dist/assets/index-D3jfzuKN.css +0 -32
- package/dist/assets/index-DTMWjJj8.js +0 -1550
- package/dist-server/server/constants/config.js +0 -6
- package/dist-server/server/constants/config.js.map +0 -1
- package/server/constants/config.ts +0 -5
|
@@ -96,6 +96,30 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
96
96
|
);
|
|
97
97
|
`;
|
|
98
98
|
|
|
99
|
+
export const SCHEDULED_MESSAGES_TABLE_SCHEMA_SQL = `
|
|
100
|
+
CREATE TABLE IF NOT EXISTS scheduled_messages (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
user_id INTEGER NOT NULL,
|
|
103
|
+
session_id TEXT NOT NULL,
|
|
104
|
+
content TEXT NOT NULL,
|
|
105
|
+
-- Composer preferences (model, effort, permission mode, attachments) as
|
|
106
|
+
-- they were when the message was scheduled, so it runs the way the user
|
|
107
|
+
-- set it up rather than however the session is configured hours later.
|
|
108
|
+
options TEXT NOT NULL DEFAULT '{}',
|
|
109
|
+
-- UTC. The client sends an absolute instant so the schedule does not move
|
|
110
|
+
-- when the user changes time zone between scheduling and firing.
|
|
111
|
+
scheduled_for DATETIME NOT NULL,
|
|
112
|
+
-- pending | sent | failed | cancelled
|
|
113
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
114
|
+
-- Why a failed one failed, shown next to it in the composer.
|
|
115
|
+
failure_reason TEXT,
|
|
116
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
117
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
118
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
119
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
120
|
+
);
|
|
121
|
+
`;
|
|
122
|
+
|
|
99
123
|
export const SESSIONS_TABLE_SCHEMA_SQL = `
|
|
100
124
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
101
125
|
session_id TEXT NOT NULL,
|
|
@@ -114,6 +138,10 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
114
138
|
-- restores its exact runtime configuration instead of provider defaults.
|
|
115
139
|
model TEXT,
|
|
116
140
|
effort TEXT,
|
|
141
|
+
-- The app session this one was branched from, NULL for sessions created
|
|
142
|
+
-- normally. Informational only: a fork is a fully independent provider
|
|
143
|
+
-- session, and deleting the source does not affect it.
|
|
144
|
+
forked_from_session_id TEXT,
|
|
117
145
|
isArchived BOOLEAN DEFAULT 0,
|
|
118
146
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
119
147
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -160,6 +188,68 @@ CREATE TABLE IF NOT EXISTS provider_models (
|
|
|
160
188
|
);
|
|
161
189
|
`;
|
|
162
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Per-user application preferences that used to live in browser localStorage.
|
|
193
|
+
*
|
|
194
|
+
* One row per (user, key); `preference_value` is always a JSON document so a
|
|
195
|
+
* key can hold a scalar (`"dark"`), a flag (`false`) or a whole settings blob
|
|
196
|
+
* without the schema having to know which. Keeping them server-side is what
|
|
197
|
+
* makes a preference follow the user from one device to another.
|
|
198
|
+
*/
|
|
199
|
+
export const USER_PREFERENCES_TABLE_SCHEMA_SQL = `
|
|
200
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
201
|
+
user_id INTEGER NOT NULL,
|
|
202
|
+
preference_key TEXT NOT NULL,
|
|
203
|
+
preference_value TEXT NOT NULL,
|
|
204
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
205
|
+
PRIMARY KEY (user_id, preference_key),
|
|
206
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
207
|
+
);
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Unsent composer text and queued messages, per user and per chat scope.
|
|
212
|
+
*
|
|
213
|
+
* `draft_scope` is a session id, or `project:<projectId>` for a chat that has
|
|
214
|
+
* not been sent yet and therefore has no session. Storing this server-side is
|
|
215
|
+
* what lets a message typed on a laptop be finished on a phone.
|
|
216
|
+
*/
|
|
217
|
+
export const SESSION_DRAFTS_TABLE_SCHEMA_SQL = `
|
|
218
|
+
CREATE TABLE IF NOT EXISTS session_drafts (
|
|
219
|
+
user_id INTEGER NOT NULL,
|
|
220
|
+
draft_scope TEXT NOT NULL,
|
|
221
|
+
draft_text TEXT NOT NULL DEFAULT '',
|
|
222
|
+
queued_message TEXT,
|
|
223
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
224
|
+
PRIMARY KEY (user_id, draft_scope),
|
|
225
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
226
|
+
);
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Provider sessions an app session has moved off, and must never move back to.
|
|
231
|
+
*
|
|
232
|
+
* Editing a message on a provider that cannot resume a transcript partway
|
|
233
|
+
* (Codex) is done by branching: the conversation is copied up to the edited
|
|
234
|
+
* turn and the app session follows the copy. The original transcript is left
|
|
235
|
+
* on disk untouched — nothing is deleted — but it is no longer the session's,
|
|
236
|
+
* and the indexer would otherwise rediscover it on its next full scan and hand
|
|
237
|
+
* the session back to the version the user edited away from.
|
|
238
|
+
*/
|
|
239
|
+
export const SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL = `
|
|
240
|
+
CREATE TABLE IF NOT EXISTS superseded_provider_sessions (
|
|
241
|
+
provider_session_id TEXT NOT NULL,
|
|
242
|
+
provider TEXT NOT NULL,
|
|
243
|
+
session_id TEXT NOT NULL,
|
|
244
|
+
-- The transcript the session left behind. Recorded because the session row
|
|
245
|
+
-- stops pointing at it, and "delete this conversation from disk" has to
|
|
246
|
+
-- reach every file the conversation ever lived in.
|
|
247
|
+
jsonl_path TEXT,
|
|
248
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
249
|
+
PRIMARY KEY (provider_session_id, provider)
|
|
250
|
+
);
|
|
251
|
+
`;
|
|
252
|
+
|
|
163
253
|
export const INIT_SCHEMA_SQL = `
|
|
164
254
|
-- Initialize authentication database
|
|
165
255
|
PRAGMA foreign_keys = ON;
|
|
@@ -207,4 +297,10 @@ ${APP_CONFIG_TABLE_SCHEMA_SQL}
|
|
|
207
297
|
${PROVIDER_MODELS_TABLE_SCHEMA_SQL}
|
|
208
298
|
CREATE INDEX IF NOT EXISTS idx_provider_models_provider_order
|
|
209
299
|
ON provider_models(provider, sort_order, id);
|
|
300
|
+
|
|
301
|
+
${USER_PREFERENCES_TABLE_SCHEMA_SQL}
|
|
302
|
+
|
|
303
|
+
${SESSION_DRAFTS_TABLE_SCHEMA_SQL}
|
|
304
|
+
|
|
305
|
+
${SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL}
|
|
210
306
|
`;
|
|
@@ -72,6 +72,58 @@ test('createSession reactivates archived rows when the session becomes active ag
|
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
test("createSession leaves an archived row archived when the transcript has not changed", async () => {
|
|
76
|
+
await withIsolatedDatabase(() => {
|
|
77
|
+
const createdAt = "2026-07-18T09:00:00.000Z";
|
|
78
|
+
const updatedAt = "2026-07-18T10:00:00.000Z";
|
|
79
|
+
const jsonlPath = "/transcripts/session-untouched.jsonl";
|
|
80
|
+
|
|
81
|
+
sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, updatedAt, jsonlPath);
|
|
82
|
+
sessionsDb.updateSessionIsArchived("session-untouched", true);
|
|
83
|
+
|
|
84
|
+
// A full rescan re-indexes every transcript created since the last scan,
|
|
85
|
+
// changed or not, and hands over the timestamps the file still carries.
|
|
86
|
+
sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, updatedAt, jsonlPath);
|
|
87
|
+
|
|
88
|
+
assert.equal(sessionsDb.getSessionById("session-untouched")?.isArchived, 1);
|
|
89
|
+
assert.equal(sessionsDb.getArchivedSessions().length, 1);
|
|
90
|
+
assert.equal(sessionsDb.getAllSessions().length, 0);
|
|
91
|
+
|
|
92
|
+
// Actually writing to the session again still brings it back.
|
|
93
|
+
sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, "2026-07-18T11:00:00.000Z", jsonlPath);
|
|
94
|
+
|
|
95
|
+
assert.equal(sessionsDb.getSessionById("session-untouched")?.isArchived, 0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the upsert path counts an omitted timestamp as activity", async () => {
|
|
100
|
+
await withIsolatedDatabase(() => {
|
|
101
|
+
// An app-created row carries no provider id, so indexing it takes the
|
|
102
|
+
// INSERT ... ON CONFLICT branch rather than the UPDATE above. Its
|
|
103
|
+
// updated_at is CURRENT_TIMESTAMP, which resolves to whole seconds, so a
|
|
104
|
+
// call in the same second is not *newer* -- the omitted timestamp itself
|
|
105
|
+
// has to be what reactivates the row.
|
|
106
|
+
sessionsDb.createAppSession("session-legacy", "claude", "/workspace/demo-project");
|
|
107
|
+
sessionsDb.updateSessionIsArchived("session-legacy", true);
|
|
108
|
+
|
|
109
|
+
sessionsDb.createSession("session-legacy", "claude", "/workspace/demo-project", "Indexed Name");
|
|
110
|
+
|
|
111
|
+
assert.equal(sessionsDb.getSessionById("session-legacy")?.isArchived, 0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the upsert path leaves an archived row alone for a transcript older than it", async () => {
|
|
116
|
+
await withIsolatedDatabase(() => {
|
|
117
|
+
sessionsDb.createAppSession("session-stale", "claude", "/workspace/demo-project");
|
|
118
|
+
sessionsDb.updateSessionIsArchived("session-stale", true);
|
|
119
|
+
|
|
120
|
+
sessionsDb.createSession("session-stale", "claude", "/workspace/demo-project", "Indexed Name", "2026-07-18T09:00:00.000Z", "2026-07-18T10:00:00.000Z", "/transcripts/session-stale.jsonl");
|
|
121
|
+
|
|
122
|
+
assert.equal(sessionsDb.getSessionById("session-stale")?.isArchived, 1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
|
|
75
127
|
test('repository reads normalize SQLite UTC timestamps to ISO strings', async () => {
|
|
76
128
|
await withIsolatedDatabase(() => {
|
|
77
129
|
sessionsDb.createAppSession('session-timezone', 'claude', '/workspace/demo-project');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
closeConnection,
|
|
9
|
+
getConnection,
|
|
10
|
+
initializeDatabase,
|
|
11
|
+
sessionDraftsDb,
|
|
12
|
+
userPreferencesDb,
|
|
13
|
+
} from '@/modules/database/index.js';
|
|
14
|
+
|
|
15
|
+
const USER_ID = 1;
|
|
16
|
+
|
|
17
|
+
async function withDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
18
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
19
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'user-prefs-db-'));
|
|
20
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
21
|
+
|
|
22
|
+
closeConnection();
|
|
23
|
+
process.env.DATABASE_PATH = databasePath;
|
|
24
|
+
await writeFile(databasePath, '');
|
|
25
|
+
await initializeDatabase();
|
|
26
|
+
|
|
27
|
+
// Both tables cascade from users(id), so a row has to exist to write against.
|
|
28
|
+
getConnection()
|
|
29
|
+
.prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)')
|
|
30
|
+
.run(USER_ID, 'tester', 'hash');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await runTest();
|
|
34
|
+
} finally {
|
|
35
|
+
closeConnection();
|
|
36
|
+
if (previousDatabasePath === undefined) {
|
|
37
|
+
delete process.env.DATABASE_PATH;
|
|
38
|
+
} else {
|
|
39
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
40
|
+
}
|
|
41
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('preferences round-trip every JSON shape a setting can take', async () => {
|
|
46
|
+
await withDatabase(() => {
|
|
47
|
+
userPreferencesDb.savePreferences(USER_ID, {
|
|
48
|
+
theme: 'dark',
|
|
49
|
+
tasksEnabled: false,
|
|
50
|
+
claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
|
|
54
|
+
theme: 'dark',
|
|
55
|
+
tasksEnabled: false,
|
|
56
|
+
claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('saving preferences merge-patches instead of replacing the whole set', async () => {
|
|
62
|
+
await withDatabase(() => {
|
|
63
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
|
|
64
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'light' });
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
|
|
67
|
+
theme: 'light',
|
|
68
|
+
userLanguage: 'de',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a preference set to undefined is removed', async () => {
|
|
74
|
+
await withDatabase(() => {
|
|
75
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
|
|
76
|
+
userPreferencesDb.savePreferences(USER_ID, { userLanguage: undefined });
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('one unreadable preference value does not cost the user the others', async () => {
|
|
83
|
+
await withDatabase(() => {
|
|
84
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark' });
|
|
85
|
+
getConnection()
|
|
86
|
+
.prepare(
|
|
87
|
+
`INSERT INTO user_preferences (user_id, preference_key, preference_value)
|
|
88
|
+
VALUES (?, ?, ?)`
|
|
89
|
+
)
|
|
90
|
+
.run(USER_ID, 'broken', 'not json');
|
|
91
|
+
|
|
92
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('drafts round-trip text and queued message per scope', async () => {
|
|
97
|
+
await withDatabase(() => {
|
|
98
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'half typed', queuedMessage: null });
|
|
99
|
+
sessionDraftsDb.saveDraft(USER_ID, 'project:p1', {
|
|
100
|
+
text: '',
|
|
101
|
+
queuedMessage: { content: 'run it' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const drafts = sessionDraftsDb.getDrafts(USER_ID);
|
|
105
|
+
const byScope = new Map(drafts.map((draft) => [draft.scope, draft]));
|
|
106
|
+
|
|
107
|
+
assert.equal(byScope.get('session-a')?.text, 'half typed');
|
|
108
|
+
assert.equal(byScope.get('session-a')?.queuedMessage, null);
|
|
109
|
+
assert.equal(byScope.get('project:p1')?.text, '');
|
|
110
|
+
assert.deepEqual(byScope.get('project:p1')?.queuedMessage, { content: 'run it' });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('saving an empty draft deletes the row rather than keeping a blank one', async () => {
|
|
115
|
+
await withDatabase(() => {
|
|
116
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'typed', queuedMessage: null });
|
|
117
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: '', queuedMessage: null });
|
|
118
|
+
|
|
119
|
+
assert.deepEqual(sessionDraftsDb.getDrafts(USER_ID), []);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('deleteDraft removes only the named scope', async () => {
|
|
124
|
+
await withDatabase(() => {
|
|
125
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'a', queuedMessage: null });
|
|
126
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-b', { text: 'b', queuedMessage: null });
|
|
127
|
+
|
|
128
|
+
sessionDraftsDb.deleteDraft(USER_ID, 'session-a');
|
|
129
|
+
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
sessionDraftsDb.getDrafts(USER_ID).map((draft) => draft.scope),
|
|
132
|
+
['session-b'],
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -11,9 +11,13 @@ import type {
|
|
|
11
11
|
} from '@/shared/types.js';
|
|
12
12
|
import { AppError, FORBIDDEN_WORKSPACE_PATHS, normalizeProjectPath } from '@/shared/utils.js';
|
|
13
13
|
|
|
14
|
+
const HARD_EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
15
|
+
'node_modules', '.git', '.svn', '.hg',
|
|
16
|
+
]);
|
|
17
|
+
|
|
14
18
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
15
|
-
|
|
16
|
-
'.
|
|
19
|
+
...HARD_EXCLUDED_DIRECTORY_NAMES,
|
|
20
|
+
'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
|
|
17
21
|
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
|
|
18
22
|
'target', 'vendor',
|
|
19
23
|
'.gradle', '.idea', 'coverage', '.nyc_output',
|
|
@@ -36,6 +40,15 @@ const MAXIMUM_FILE_TREE_ENTRIES = 10_000;
|
|
|
36
40
|
|
|
37
41
|
type FileTreeEntryFilter = (entryPath: string, isDirectory: boolean) => boolean;
|
|
38
42
|
|
|
43
|
+
function includeEntryByHardExclusions(entryPath: string, isDirectory: boolean): boolean {
|
|
44
|
+
return !isDirectory || !HARD_EXCLUDED_DIRECTORY_NAMES.has(path.basename(entryPath));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function includeEntryByFallbackDirectoryNames(entryPath: string, isDirectory: boolean): boolean {
|
|
48
|
+
return includeEntryByHardExclusions(entryPath, isDirectory)
|
|
49
|
+
&& (!isDirectory || !IGNORED_DIRECTORY_NAMES.has(path.basename(entryPath)));
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
function createFileTreeError(message: string, statusCode: number, code: string): AppError {
|
|
40
53
|
return new AppError(message, { statusCode, code });
|
|
41
54
|
}
|
|
@@ -146,6 +159,8 @@ function createGitignoreEntryFilter(
|
|
|
146
159
|
const gitignore = ignore().add(gitignoreContent);
|
|
147
160
|
|
|
148
161
|
return (entryPath, isDirectory) => {
|
|
162
|
+
if (!includeEntryByHardExclusions(entryPath, isDirectory)) return false;
|
|
163
|
+
|
|
149
164
|
const relativePath = path.relative(projectRoot, entryPath).split(path.sep).join('/');
|
|
150
165
|
const matchPath = isDirectory ? `${relativePath}/` : relativePath;
|
|
151
166
|
return !gitignore.ignores(matchPath);
|
|
@@ -191,17 +206,13 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
191
206
|
try {
|
|
192
207
|
for await (const entry of fileSystem.openDirectory(directoryPath)) {
|
|
193
208
|
const isDirectory = entry.isDirectory();
|
|
194
|
-
if (isDirectory && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
209
|
if (!includeEntry(path.join(directoryPath, entry.name), isDirectory)) {
|
|
198
210
|
continue;
|
|
199
211
|
}
|
|
200
212
|
|
|
201
|
-
// Charged after
|
|
202
|
-
// .
|
|
203
|
-
//
|
|
204
|
-
// rather than per directory.
|
|
213
|
+
// Charged after visibility filtering so ignored entries never consume
|
|
214
|
+
// the budget. Every recursive branch shares one counter, so the cap
|
|
215
|
+
// applies to the whole tree rather than per directory.
|
|
205
216
|
if (remainingEntries.value <= 0) {
|
|
206
217
|
throw createFileTreeError(
|
|
207
218
|
`Project file tree exceeds the ${MAXIMUM_FILE_TREE_ENTRIES.toLocaleString()} entry limit. Choose a narrower project directory or add ignore rules.`,
|
|
@@ -233,7 +244,7 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
233
244
|
directoryPath: string,
|
|
234
245
|
maximumDepth: number,
|
|
235
246
|
currentDepth = 0,
|
|
236
|
-
includeEntry: FileTreeEntryFilter =
|
|
247
|
+
includeEntry: FileTreeEntryFilter = includeEntryByFallbackDirectoryNames,
|
|
237
248
|
remainingEntries = { value: MAXIMUM_FILE_TREE_ENTRIES },
|
|
238
249
|
): Promise<FileTreeNode[]> {
|
|
239
250
|
const visibleEntries = await collectVisibleEntries(directoryPath, includeEntry, remainingEntries);
|
|
@@ -87,37 +87,67 @@ function createDependencies(
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
test('listProjectFiles
|
|
90
|
+
test('listProjectFiles applies gitignore alongside hard directory exclusions', async () => {
|
|
91
91
|
const projectRoot = path.resolve('file-tree-test-project');
|
|
92
|
+
const documentationDirectory = path.join(projectRoot, 'docs');
|
|
93
|
+
const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
|
|
94
|
+
const gitDirectory = path.join(projectRoot, '.git');
|
|
95
|
+
const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
|
|
92
96
|
const sourceDirectory = path.join(projectRoot, 'src');
|
|
97
|
+
const readDirectories: string[] = [];
|
|
93
98
|
const fileSystem = createFakeFileSystem({
|
|
94
99
|
access: async () => undefined,
|
|
100
|
+
readTextFile: async (filePath) => {
|
|
101
|
+
assert.equal(filePath, path.join(projectRoot, '.gitignore'));
|
|
102
|
+
return '*.log';
|
|
103
|
+
},
|
|
95
104
|
openDirectory: createDirectoryReader((directoryPath) => {
|
|
105
|
+
readDirectories.push(directoryPath);
|
|
96
106
|
if (directoryPath === projectRoot) {
|
|
97
107
|
return [
|
|
108
|
+
createDirectoryEntry('.git', true),
|
|
98
109
|
createDirectoryEntry('node_modules', true),
|
|
99
110
|
createDirectoryEntry('README.md', false),
|
|
111
|
+
createDirectoryEntry('docs', true),
|
|
100
112
|
createDirectoryEntry('src', true),
|
|
101
113
|
];
|
|
102
114
|
}
|
|
115
|
+
if (directoryPath === documentationDirectory) {
|
|
116
|
+
return [createDirectoryEntry('build', true)];
|
|
117
|
+
}
|
|
118
|
+
if (directoryPath === buildDocumentationDirectory) {
|
|
119
|
+
return [createDirectoryEntry('foo.md', false)];
|
|
120
|
+
}
|
|
103
121
|
if (directoryPath === sourceDirectory) {
|
|
104
122
|
return [createDirectoryEntry('index.ts', false)];
|
|
105
123
|
}
|
|
106
124
|
return [];
|
|
107
125
|
}),
|
|
108
|
-
lstat: async (candidatePath) => createStats(
|
|
126
|
+
lstat: async (candidatePath) => createStats(
|
|
127
|
+
candidatePath === documentationDirectory
|
|
128
|
+
|| candidatePath === buildDocumentationDirectory
|
|
129
|
+
|| candidatePath === sourceDirectory
|
|
130
|
+
|| candidatePath === gitDirectory
|
|
131
|
+
|| candidatePath === nodeModulesDirectory,
|
|
132
|
+
0o754,
|
|
133
|
+
),
|
|
109
134
|
});
|
|
110
135
|
const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
|
|
111
136
|
|
|
112
|
-
const tree = await service.listProjectFiles('project-1');
|
|
137
|
+
const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
|
|
113
138
|
|
|
114
|
-
assert.deepEqual(tree.map((entry) => entry.name), ['src', 'README.md']);
|
|
115
|
-
const
|
|
139
|
+
assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'src', 'README.md']);
|
|
140
|
+
const documentationEntry = tree[0];
|
|
141
|
+
assert.deepEqual(documentationEntry?.children?.map((entry) => entry.name), ['build']);
|
|
142
|
+
assert.deepEqual(documentationEntry?.children?.[0]?.children?.map((entry) => entry.name), ['foo.md']);
|
|
143
|
+
const sourceEntry = tree[1];
|
|
116
144
|
assert.ok(sourceEntry);
|
|
117
145
|
assert.equal(sourceEntry.type, 'directory');
|
|
118
146
|
assert.equal(sourceEntry.permissions, '754');
|
|
119
147
|
assert.equal(sourceEntry.permissionsRwx, 'rwxr-xr--');
|
|
120
148
|
assert.deepEqual(sourceEntry.children?.map((entry) => entry.name), ['index.ts']);
|
|
149
|
+
assert.equal(readDirectories.includes(gitDirectory), false);
|
|
150
|
+
assert.equal(readDirectories.includes(nodeModulesDirectory), false);
|
|
121
151
|
});
|
|
122
152
|
|
|
123
153
|
test('listProjectFiles excludes gitignored entries only when requested', async () => {
|
|
@@ -167,23 +197,47 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
|
|
|
167
197
|
assert.equal(readDirectories.includes(cacheDirectory), false);
|
|
168
198
|
});
|
|
169
199
|
|
|
170
|
-
test('listProjectFiles
|
|
200
|
+
test('listProjectFiles falls back to conventional directory names when no gitignore exists', async () => {
|
|
171
201
|
const projectRoot = path.resolve('file-tree-test-project');
|
|
202
|
+
const documentationDirectory = path.join(projectRoot, 'docs');
|
|
203
|
+
const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
|
|
204
|
+
const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
|
|
205
|
+
const readDirectories: string[] = [];
|
|
172
206
|
const fileSystem = createFakeFileSystem({
|
|
173
207
|
access: async () => undefined,
|
|
174
208
|
readTextFile: async () => {
|
|
175
209
|
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
176
210
|
},
|
|
177
|
-
openDirectory: createDirectoryReader((directoryPath) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
openDirectory: createDirectoryReader((directoryPath) => {
|
|
212
|
+
readDirectories.push(directoryPath);
|
|
213
|
+
if (directoryPath === projectRoot) {
|
|
214
|
+
return [
|
|
215
|
+
createDirectoryEntry('debug.log', false),
|
|
216
|
+
createDirectoryEntry('docs', true),
|
|
217
|
+
createDirectoryEntry('node_modules', true),
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
if (directoryPath === documentationDirectory) {
|
|
221
|
+
return [
|
|
222
|
+
createDirectoryEntry('build', true),
|
|
223
|
+
createDirectoryEntry('guide.md', false),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
if (directoryPath === buildDocumentationDirectory) {
|
|
227
|
+
return [createDirectoryEntry('generated.md', false)];
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}),
|
|
231
|
+
lstat: async (candidatePath) => createStats(candidatePath === documentationDirectory, 0o644),
|
|
181
232
|
});
|
|
182
233
|
const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
|
|
183
234
|
|
|
184
235
|
const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
|
|
185
236
|
|
|
186
|
-
assert.deepEqual(tree.map((entry) => entry.name), ['debug.log']);
|
|
237
|
+
assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'debug.log']);
|
|
238
|
+
assert.deepEqual(tree[0]?.children?.map((entry) => entry.name), ['guide.md']);
|
|
239
|
+
assert.equal(readDirectories.includes(nodeModulesDirectory), false);
|
|
240
|
+
assert.equal(readDirectories.includes(buildDocumentationDirectory), false);
|
|
187
241
|
});
|
|
188
242
|
|
|
189
243
|
test('listProjectFiles rejects a tree that exceeds the server entry limit', async () => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AppError } from '@/shared/utils.js';
|
|
2
|
+
|
|
3
|
+
type GitCommandResult = {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type GitCommandRunner = (
|
|
9
|
+
command: string,
|
|
10
|
+
args: string[],
|
|
11
|
+
options: { cwd: string },
|
|
12
|
+
) => Promise<GitCommandResult>;
|
|
13
|
+
|
|
14
|
+
type DeleteLocalBranchInput = {
|
|
15
|
+
projectPath: string;
|
|
16
|
+
branch: string;
|
|
17
|
+
force: boolean;
|
|
18
|
+
runCommand: GitCommandRunner;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Used by the Git routes module to safely delete a non-current local branch. */
|
|
22
|
+
export async function deleteLocalBranch(input: DeleteLocalBranchInput): Promise<string> {
|
|
23
|
+
const { stdout: currentBranch } = await input.runCommand(
|
|
24
|
+
'git',
|
|
25
|
+
['branch', '--show-current'],
|
|
26
|
+
{ cwd: input.projectPath },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (currentBranch.trim() === input.branch) {
|
|
30
|
+
throw new AppError('Cannot delete the currently checked-out branch', {
|
|
31
|
+
code: 'GIT_CURRENT_BRANCH_DELETE',
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const deleteFlag = input.force ? '-D' : '-d';
|
|
37
|
+
const { stdout } = await input.runCommand(
|
|
38
|
+
'git',
|
|
39
|
+
['branch', deleteFlag, '--', input.branch],
|
|
40
|
+
{ cwd: input.projectPath },
|
|
41
|
+
);
|
|
42
|
+
return stdout;
|
|
43
|
+
}
|
|
@@ -8,6 +8,7 @@ import { AppError } from '@/shared/utils.js';
|
|
|
8
8
|
|
|
9
9
|
// cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
|
|
10
10
|
import { parseGitLogWithStats, parseGitStatusOutput } from './git-parsing.service.js';
|
|
11
|
+
import { deleteLocalBranch } from './git-branch.service.js';
|
|
11
12
|
|
|
12
13
|
type GitRouterDependencies = {
|
|
13
14
|
fileSystem: typeof import('node:fs/promises');
|
|
@@ -839,26 +840,32 @@ router.post('/create-branch', async (req, res) => {
|
|
|
839
840
|
|
|
840
841
|
// Delete a local branch
|
|
841
842
|
router.post('/delete-branch', async (req, res) => {
|
|
842
|
-
const { project, branch } = req.body;
|
|
843
|
+
const { project, branch, force = false } = req.body;
|
|
843
844
|
|
|
844
845
|
if (!project || !branch) {
|
|
845
846
|
return res.status(400).json({ error: 'Project id and branch name are required' });
|
|
846
847
|
}
|
|
847
848
|
|
|
849
|
+
if (typeof force !== 'boolean') {
|
|
850
|
+
return res.status(400).json({ error: 'force must be a boolean' });
|
|
851
|
+
}
|
|
852
|
+
|
|
848
853
|
try {
|
|
849
854
|
const projectPath = await getActualProjectPath(project);
|
|
850
855
|
await validateGitRepository(projectPath);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const { stdout } = await spawnAsync('git', ['branch', '-d', branch], { cwd: projectPath });
|
|
856
|
+
validateBranchName(branch);
|
|
857
|
+
const stdout = await deleteLocalBranch({
|
|
858
|
+
projectPath,
|
|
859
|
+
branch,
|
|
860
|
+
force,
|
|
861
|
+
runCommand: spawnAsync,
|
|
862
|
+
});
|
|
859
863
|
res.json({ success: true, output: stdout });
|
|
860
864
|
} catch (error) {
|
|
861
865
|
console.error('Git delete branch error:', error);
|
|
866
|
+
if (error instanceof AppError) {
|
|
867
|
+
return res.status(error.statusCode).json({ error: error.message });
|
|
868
|
+
}
|
|
862
869
|
res.status(500).json({ error: error.message });
|
|
863
870
|
}
|
|
864
871
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { deleteLocalBranch } from '../git-branch.service.js';
|
|
5
|
+
|
|
6
|
+
test('deleteLocalBranch uses safe deletion by default', async () => {
|
|
7
|
+
const commands: string[][] = [];
|
|
8
|
+
const output = await deleteLocalBranch({
|
|
9
|
+
projectPath: '/workspace/repo',
|
|
10
|
+
branch: 'feature/safe-delete',
|
|
11
|
+
force: false,
|
|
12
|
+
runCommand: async (_command, args) => {
|
|
13
|
+
commands.push(args);
|
|
14
|
+
return {
|
|
15
|
+
stdout: args.includes('--show-current') ? 'main\n' : 'Deleted branch feature/safe-delete.\n',
|
|
16
|
+
stderr: '',
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(output, 'Deleted branch feature/safe-delete.\n');
|
|
22
|
+
assert.deepEqual(commands, [
|
|
23
|
+
['branch', '--show-current'],
|
|
24
|
+
['branch', '-d', '--', 'feature/safe-delete'],
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('deleteLocalBranch uses force deletion only when requested', async () => {
|
|
29
|
+
const commands: string[][] = [];
|
|
30
|
+
await deleteLocalBranch({
|
|
31
|
+
projectPath: '/workspace/repo',
|
|
32
|
+
branch: 'feature/unmerged',
|
|
33
|
+
force: true,
|
|
34
|
+
runCommand: async (_command, args) => {
|
|
35
|
+
commands.push(args);
|
|
36
|
+
return { stdout: args.includes('--show-current') ? 'main\n' : '', stderr: '' };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(commands, [
|
|
41
|
+
['branch', '--show-current'],
|
|
42
|
+
['branch', '-D', '--', 'feature/unmerged'],
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('deleteLocalBranch refuses to delete the current branch even when forced', async () => {
|
|
47
|
+
await assert.rejects(
|
|
48
|
+
deleteLocalBranch({
|
|
49
|
+
projectPath: '/workspace/repo',
|
|
50
|
+
branch: 'feature/current',
|
|
51
|
+
force: true,
|
|
52
|
+
runCommand: async () => ({ stdout: 'feature/current\n', stderr: '' }),
|
|
53
|
+
}),
|
|
54
|
+
(error: unknown) => {
|
|
55
|
+
assert.equal((error as { code?: string }).code, 'GIT_CURRENT_BRANCH_DELETE');
|
|
56
|
+
assert.equal((error as { statusCode?: number }).statusCode, 400);
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
});
|