@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, 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 { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
8
|
+
|
|
9
|
+
// Each provider synchronizer resolves `os.homedir()` when the registry module is
|
|
10
|
+
// first imported, so HOME has to point at an empty fixture home *before* that
|
|
11
|
+
// import runs. Otherwise the sync pass walks the developer's real ~/.claude.
|
|
12
|
+
const fixtureHome = await mkdtemp(path.join(os.tmpdir(), 'session-prune-home-'));
|
|
13
|
+
const previousHome = process.env.HOME;
|
|
14
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
15
|
+
process.env.HOME = fixtureHome;
|
|
16
|
+
process.env.USERPROFILE = fixtureHome;
|
|
17
|
+
|
|
18
|
+
const { sessionSynchronizerService } = await import(
|
|
19
|
+
'@/modules/providers/services/session-synchronizer.service.js'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
process.on('exit', () => {
|
|
23
|
+
if (previousHome === undefined) {
|
|
24
|
+
delete process.env.HOME;
|
|
25
|
+
} else {
|
|
26
|
+
process.env.HOME = previousHome;
|
|
27
|
+
}
|
|
28
|
+
if (previousUserProfile === undefined) {
|
|
29
|
+
delete process.env.USERPROFILE;
|
|
30
|
+
} else {
|
|
31
|
+
process.env.USERPROFILE = previousUserProfile;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function withIsolatedDatabase(runTest: (workspace: string) => Promise<void>): Promise<void> {
|
|
36
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
37
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-prune-db-'));
|
|
38
|
+
|
|
39
|
+
closeConnection();
|
|
40
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
41
|
+
await initializeDatabase();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await runTest(tempDirectory);
|
|
45
|
+
} finally {
|
|
46
|
+
closeConnection();
|
|
47
|
+
if (previousDatabasePath === undefined) {
|
|
48
|
+
delete process.env.DATABASE_PATH;
|
|
49
|
+
} else {
|
|
50
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
51
|
+
}
|
|
52
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PROJECT_PATH = '/tmp/session-prune-project';
|
|
57
|
+
|
|
58
|
+
test('synchronizeSessions drops indexed sessions whose transcript file was deleted', async () => {
|
|
59
|
+
await withIsolatedDatabase(async (workspace) => {
|
|
60
|
+
const transcriptDirectory = path.join(workspace, 'transcripts');
|
|
61
|
+
await mkdir(transcriptDirectory, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const livePath = path.join(transcriptDirectory, 'live.jsonl');
|
|
64
|
+
await writeFile(livePath, '{}\n');
|
|
65
|
+
const deletedPath = path.join(transcriptDirectory, 'deleted.jsonl');
|
|
66
|
+
|
|
67
|
+
sessionsDb.createSession('live-session', 'claude', PROJECT_PATH, 'Live', undefined, undefined, livePath);
|
|
68
|
+
sessionsDb.createSession('orphan-session', 'claude', PROJECT_PATH, 'Untitled Claude Session', undefined, undefined, deletedPath);
|
|
69
|
+
|
|
70
|
+
const result = await sessionSynchronizerService.synchronizeSessions();
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(result.failures, []);
|
|
73
|
+
assert.equal(result.prunedOrphans, 1);
|
|
74
|
+
assert.equal(sessionsDb.getSessionById('orphan-session'), null);
|
|
75
|
+
assert.ok(sessionsDb.getSessionById('live-session'), 'a session whose transcript still exists must survive');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('synchronizeSessions keeps sessions whose whole transcript directory is missing', async () => {
|
|
80
|
+
await withIsolatedDatabase(async (workspace) => {
|
|
81
|
+
// Stands in for an unmounted or not-yet-created home: every transcript
|
|
82
|
+
// "looks" deleted, so pruning here would wipe the entire index.
|
|
83
|
+
const unmountedPath = path.join(workspace, 'not-mounted', 'session.jsonl');
|
|
84
|
+
|
|
85
|
+
sessionsDb.createSession('unmounted-session', 'claude', PROJECT_PATH, 'Unmounted', undefined, undefined, unmountedPath);
|
|
86
|
+
|
|
87
|
+
const result = await sessionSynchronizerService.synchronizeSessions();
|
|
88
|
+
|
|
89
|
+
assert.equal(result.prunedOrphans, 0);
|
|
90
|
+
assert.ok(sessionsDb.getSessionById('unmounted-session'));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('synchronizeSessions keeps sessions that have no transcript path yet', async () => {
|
|
95
|
+
await withIsolatedDatabase(async () => {
|
|
96
|
+
// App-created rows (jsonl_path NULL until the first provider write) and
|
|
97
|
+
// OpenCode rows (one shared sqlite file, so jsonl_path stays NULL).
|
|
98
|
+
sessionsDb.createAppSession('pending-app-session', 'claude', PROJECT_PATH, 'Pending');
|
|
99
|
+
sessionsDb.createSession('opencode-session', 'opencode', PROJECT_PATH, 'OpenCode', undefined, undefined, null);
|
|
100
|
+
|
|
101
|
+
const result = await sessionSynchronizerService.synchronizeSessions();
|
|
102
|
+
|
|
103
|
+
assert.equal(result.prunedOrphans, 0);
|
|
104
|
+
assert.ok(sessionsDb.getSessionById('pending-app-session'));
|
|
105
|
+
assert.ok(sessionsDb.getSessionById('opencode-session'));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } 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 { closeConnection, initializeDatabase } from '@/modules/database/index.js';
|
|
8
|
+
import type { IProvider } from '@/shared/interfaces.js';
|
|
9
|
+
|
|
10
|
+
// Each provider synchronizer resolves `os.homedir()` when the registry module is
|
|
11
|
+
// first imported, so HOME has to point at an empty fixture home *before* that
|
|
12
|
+
// import runs. The registry is stubbed per test, but the import still happens.
|
|
13
|
+
const fixtureHome = await mkdtemp(path.join(os.tmpdir(), 'session-sync-home-'));
|
|
14
|
+
const previousHome = process.env.HOME;
|
|
15
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
16
|
+
process.env.HOME = fixtureHome;
|
|
17
|
+
process.env.USERPROFILE = fixtureHome;
|
|
18
|
+
|
|
19
|
+
const { providerRegistry } = await import('@/modules/providers/provider.registry.js');
|
|
20
|
+
const { sessionSynchronizerService } = await import(
|
|
21
|
+
'@/modules/providers/services/session-synchronizer.service.js'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
process.on('exit', () => {
|
|
25
|
+
if (previousHome === undefined) {
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
} else {
|
|
28
|
+
process.env.HOME = previousHome;
|
|
29
|
+
}
|
|
30
|
+
if (previousUserProfile === undefined) {
|
|
31
|
+
delete process.env.USERPROFILE;
|
|
32
|
+
} else {
|
|
33
|
+
process.env.USERPROFILE = previousUserProfile;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function withIsolatedDatabase(runTest: () => Promise<void>): Promise<void> {
|
|
38
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
39
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-sync-db-'));
|
|
40
|
+
|
|
41
|
+
closeConnection();
|
|
42
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
43
|
+
await initializeDatabase();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await runTest();
|
|
47
|
+
} finally {
|
|
48
|
+
closeConnection();
|
|
49
|
+
if (previousDatabasePath === undefined) {
|
|
50
|
+
delete process.env.DATABASE_PATH;
|
|
51
|
+
} else {
|
|
52
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
53
|
+
}
|
|
54
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Installs a single fake provider whose scan only settles when the returned
|
|
60
|
+
* `release` is called, so a second caller can be observed arriving mid-scan.
|
|
61
|
+
*/
|
|
62
|
+
function stubSlowProvider() {
|
|
63
|
+
const originalListProviders = providerRegistry.listProviders;
|
|
64
|
+
let synchronizeCalls = 0;
|
|
65
|
+
let releaseScan: () => void = () => {};
|
|
66
|
+
const scanStarted = new Promise<void>((resolveStarted) => {
|
|
67
|
+
const fakeProvider = {
|
|
68
|
+
id: 'claude',
|
|
69
|
+
sessionSynchronizer: {
|
|
70
|
+
async synchronize() {
|
|
71
|
+
synchronizeCalls += 1;
|
|
72
|
+
resolveStarted();
|
|
73
|
+
await new Promise<void>((resolveScan) => {
|
|
74
|
+
releaseScan = resolveScan;
|
|
75
|
+
});
|
|
76
|
+
return 7;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
} as unknown as IProvider;
|
|
80
|
+
|
|
81
|
+
providerRegistry.listProviders = () => [fakeProvider];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
scanStarted,
|
|
86
|
+
release: () => releaseScan(),
|
|
87
|
+
getSynchronizeCalls: () => synchronizeCalls,
|
|
88
|
+
restore: () => {
|
|
89
|
+
providerRegistry.listProviders = originalListProviders;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('concurrent synchronizeSessions callers share one provider scan', async () => {
|
|
95
|
+
await withIsolatedDatabase(async () => {
|
|
96
|
+
const providerStub = stubSlowProvider();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const first = sessionSynchronizerService.synchronizeSessions();
|
|
100
|
+
await providerStub.scanStarted;
|
|
101
|
+
// Opening the UI fires /api/projects and /api/projects/archived at once;
|
|
102
|
+
// the second must join the running scan instead of starting its own.
|
|
103
|
+
const second = sessionSynchronizerService.synchronizeSessions();
|
|
104
|
+
|
|
105
|
+
providerStub.release();
|
|
106
|
+
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
107
|
+
|
|
108
|
+
assert.equal(providerStub.getSynchronizeCalls(), 1);
|
|
109
|
+
assert.equal(firstResult.processedByProvider.claude, 7);
|
|
110
|
+
assert.deepEqual(secondResult, firstResult);
|
|
111
|
+
} finally {
|
|
112
|
+
providerStub.restore();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a synchronizeSessions call after the shared scan settles starts a new scan', async () => {
|
|
118
|
+
await withIsolatedDatabase(async () => {
|
|
119
|
+
const providerStub = stubSlowProvider();
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const first = sessionSynchronizerService.synchronizeSessions();
|
|
123
|
+
await providerStub.scanStarted;
|
|
124
|
+
providerStub.release();
|
|
125
|
+
await first;
|
|
126
|
+
|
|
127
|
+
const second = sessionSynchronizerService.synchronizeSessions();
|
|
128
|
+
providerStub.release();
|
|
129
|
+
await second;
|
|
130
|
+
|
|
131
|
+
assert.equal(providerStub.getSynchronizeCalls(), 2);
|
|
132
|
+
} finally {
|
|
133
|
+
providerStub.restore();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
@@ -125,3 +125,69 @@ test('recent sessions map project metadata and preserve database pagination', {
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
/** One Claude transcript row of user or assistant text, linked by uuid chain. */
|
|
130
|
+
function claudeTextRow(
|
|
131
|
+
sessionId: string,
|
|
132
|
+
role: 'user' | 'assistant',
|
|
133
|
+
text: string,
|
|
134
|
+
ordinal: number,
|
|
135
|
+
): Record<string, unknown> {
|
|
136
|
+
return {
|
|
137
|
+
type: role,
|
|
138
|
+
uuid: `row-${ordinal}`,
|
|
139
|
+
parentUuid: ordinal === 0 ? null : `row-${ordinal - 1}`,
|
|
140
|
+
timestamp: new Date(Date.UTC(2026, 0, 1, 0, 0, ordinal)).toISOString(),
|
|
141
|
+
sessionId,
|
|
142
|
+
message: { role, content: [{ type: 'text', text }] },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test('history pages are sliced from the cached full transcript and see appended rows', { concurrency: false }, async () => {
|
|
147
|
+
const transcriptDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-history-'));
|
|
148
|
+
const sessionId = 'claude-history-cache-session';
|
|
149
|
+
const transcriptPath = path.join(transcriptDirectory, `${sessionId}.jsonl`);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await withIsolatedDatabase(async () => {
|
|
153
|
+
const rows = [
|
|
154
|
+
claudeTextRow(sessionId, 'user', 'one', 0),
|
|
155
|
+
claudeTextRow(sessionId, 'assistant', 'reply one', 1),
|
|
156
|
+
claudeTextRow(sessionId, 'user', 'two', 2),
|
|
157
|
+
claudeTextRow(sessionId, 'assistant', 'reply two', 3),
|
|
158
|
+
];
|
|
159
|
+
await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
|
|
160
|
+
sessionsDb.createSession(
|
|
161
|
+
sessionId,
|
|
162
|
+
'claude',
|
|
163
|
+
'/tmp/history-cache-project',
|
|
164
|
+
'History cache conversation',
|
|
165
|
+
'2026-01-01T00:00:00.000Z',
|
|
166
|
+
'2026-01-01T00:00:10.000Z',
|
|
167
|
+
transcriptPath,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const page = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
|
|
171
|
+
assert.equal(page.total, 4);
|
|
172
|
+
assert.equal(page.hasMore, true);
|
|
173
|
+
assert.deepEqual(page.messages.map((message) => message.content), ['two', 'reply two']);
|
|
174
|
+
|
|
175
|
+
// A row appended after the page was cached must appear on the next read.
|
|
176
|
+
await appendFile(
|
|
177
|
+
transcriptPath,
|
|
178
|
+
`${JSON.stringify(claudeTextRow(sessionId, 'user', 'three', 4))}\n`,
|
|
179
|
+
'utf8',
|
|
180
|
+
);
|
|
181
|
+
const refreshed = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
|
|
182
|
+
assert.equal(refreshed.total, 5);
|
|
183
|
+
assert.deepEqual(refreshed.messages.map((message) => message.content), ['reply two', 'three']);
|
|
184
|
+
|
|
185
|
+
// An older page keeps the tail-offset contract while served from cache.
|
|
186
|
+
const older = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 2 });
|
|
187
|
+
assert.deepEqual(older.messages.map((message) => message.content), ['reply one', 'two']);
|
|
188
|
+
assert.equal(older.hasMore, true);
|
|
189
|
+
});
|
|
190
|
+
} finally {
|
|
191
|
+
await rm(transcriptDirectory, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
@@ -69,7 +69,8 @@ const writeClaudePluginCommand = async (
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* This test covers Claude user/project skill folders plus plugin discovery from
|
|
72
|
-
* installed plugin command files and
|
|
72
|
+
* installed plugin command files and plugin skill files, including a plugin that
|
|
73
|
+
* ships both.
|
|
73
74
|
*/
|
|
74
75
|
test('providerSkillsService lists claude user, project, and enabled plugin skills', { concurrency: false }, async () => {
|
|
75
76
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
|
|
@@ -144,11 +145,19 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
|
|
|
144
145
|
);
|
|
145
146
|
await writeSkill(
|
|
146
147
|
path.join(commandPluginInstallPath, 'skills'),
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
148
|
+
'command-plugin-skill-dir',
|
|
149
|
+
'command-plugin-skill',
|
|
150
|
+
'Skill shipped beside plugin commands',
|
|
150
151
|
);
|
|
151
152
|
await writeClaudePluginManifest(skillPluginInstallPath, 'ExampleSkills');
|
|
153
|
+
// A commands folder holding nothing this reader takes must not hide the
|
|
154
|
+
// skills beside it: plugins do ship commands in other agents' formats.
|
|
155
|
+
await fs.mkdir(path.join(skillPluginInstallPath, 'commands'), { recursive: true });
|
|
156
|
+
await fs.writeFile(
|
|
157
|
+
path.join(skillPluginInstallPath, 'commands', 'other-agent.toml'),
|
|
158
|
+
'description = "Command for another agent"\n',
|
|
159
|
+
'utf8',
|
|
160
|
+
);
|
|
152
161
|
await writeSkill(
|
|
153
162
|
path.join(skillPluginInstallPath, 'skills'),
|
|
154
163
|
'claude-plugin-dir',
|
|
@@ -280,7 +289,14 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
|
|
|
280
289
|
assert.equal(pluginCommand?.command, '/Notion:insert-row');
|
|
281
290
|
assert.equal(pluginCommand?.description, 'Insert a Notion database row');
|
|
282
291
|
assert.match(pluginCommand?.sourcePath ?? '', /commands[\\/]insert-row\.md$/);
|
|
283
|
-
|
|
292
|
+
// A plugin that ships commands keeps its skills: the CLI offers both, so
|
|
293
|
+
// the menu lists both rather than letting the commands folder hide them.
|
|
294
|
+
const commandPluginSkill = byName.get('command-plugin-skill');
|
|
295
|
+
assert.equal(commandPluginSkill?.scope, 'plugin');
|
|
296
|
+
assert.equal(commandPluginSkill?.pluginName, 'Notion');
|
|
297
|
+
assert.equal(commandPluginSkill?.pluginId, 'notion@notion-marketplace');
|
|
298
|
+
assert.equal(commandPluginSkill?.command, '/Notion:command-plugin-skill');
|
|
299
|
+
assert.equal(commandPluginSkill?.description, 'Skill shipped beside plugin commands');
|
|
284
300
|
|
|
285
301
|
const pluginSkill = byName.get('claude-plugin');
|
|
286
302
|
assert.equal(pluginSkill?.scope, 'plugin');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The HTTP surface for scheduling a message to a session, mounted by the app.
|
|
2
|
+
export { default as scheduledMessagesRoutes } from './scheduled-messages.routes.js';
|
|
3
|
+
|
|
4
|
+
// The timer that sends them, started and stopped with the server.
|
|
5
|
+
export {
|
|
6
|
+
initializeScheduledMessageDispatcher,
|
|
7
|
+
closeScheduledMessageDispatcher,
|
|
8
|
+
} from './services/scheduled-message-dispatcher.service.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { scheduledMessagesService } from '@/modules/scheduled-messages/services/scheduled-messages.service.js';
|
|
5
|
+
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
6
|
+
|
|
7
|
+
type AuthenticatedRequest = Request & { user?: { id?: number | string } };
|
|
8
|
+
|
|
9
|
+
function readUserId(request: Request): number {
|
|
10
|
+
const userId = Number((request as AuthenticatedRequest).user?.id);
|
|
11
|
+
if (!Number.isInteger(userId)) {
|
|
12
|
+
throw new AppError('Authenticated user is required.', {
|
|
13
|
+
code: 'USER_REQUIRED',
|
|
14
|
+
statusCode: 401,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return userId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readString(value: unknown, field: string): string {
|
|
21
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
22
|
+
throw new AppError(`${field} is required.`, {
|
|
23
|
+
code: 'INVALID_REQUEST_BODY',
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const router = express.Router();
|
|
31
|
+
|
|
32
|
+
router.get(
|
|
33
|
+
'/',
|
|
34
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
35
|
+
const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : '';
|
|
36
|
+
const userId = readUserId(req);
|
|
37
|
+
res.json(createApiSuccessResponse(
|
|
38
|
+
sessionId
|
|
39
|
+
? scheduledMessagesService.listForSession(userId, sessionId)
|
|
40
|
+
: scheduledMessagesService.listPending(userId),
|
|
41
|
+
));
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
router.post(
|
|
46
|
+
'/',
|
|
47
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
48
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
49
|
+
const result = scheduledMessagesService.schedule({
|
|
50
|
+
userId: readUserId(req),
|
|
51
|
+
sessionId: readString(body.sessionId, 'sessionId'),
|
|
52
|
+
content: readString(body.content, 'content'),
|
|
53
|
+
options: body.options,
|
|
54
|
+
scheduledFor: readString(body.scheduledFor, 'scheduledFor'),
|
|
55
|
+
});
|
|
56
|
+
res.status(201).json(createApiSuccessResponse(result));
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
router.delete(
|
|
61
|
+
'/:id',
|
|
62
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
63
|
+
scheduledMessagesService.cancel(readUserId(req), readString(req.params.id, 'id'));
|
|
64
|
+
res.json(createApiSuccessResponse({ cancelled: true }));
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export default router;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { scheduledMessagesDb, sessionDraftsDb } from '@/modules/database/index.js';
|
|
2
|
+
import type { QueuedSessionMessageRecord, ScheduledMessageRow } from '@/modules/database/index.js';
|
|
3
|
+
import { chatRunRegistry, runDetachedChatTurn } from '@/modules/websocket/index.js';
|
|
4
|
+
import type { ProviderRuntimeGateway } from '@/modules/websocket/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How often due messages are looked for.
|
|
8
|
+
*
|
|
9
|
+
* A minute is the granularity the composer offers, and a claim is indexed on
|
|
10
|
+
* `(status, scheduled_for)`, so the poll is one cheap query. Anything finer
|
|
11
|
+
* would buy precision nobody asked for.
|
|
12
|
+
*/
|
|
13
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
16
|
+
let dispatchInFlight = false;
|
|
17
|
+
|
|
18
|
+
type StoredQueuedMessage = {
|
|
19
|
+
content: string;
|
|
20
|
+
options: Record<string, unknown>;
|
|
21
|
+
attachments: unknown[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function readOptions(raw: string): Record<string, unknown> {
|
|
25
|
+
try {
|
|
26
|
+
const parsed: unknown = JSON.parse(raw);
|
|
27
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
28
|
+
? parsed as Record<string, unknown>
|
|
29
|
+
: {};
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readQueuedMessage(value: unknown): StoredQueuedMessage | null {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const record = value as Record<string, unknown>;
|
|
40
|
+
const content = typeof record.content === 'string' ? record.content : '';
|
|
41
|
+
const attachments = Array.isArray(record.attachments)
|
|
42
|
+
? record.attachments
|
|
43
|
+
: Array.isArray(record.images)
|
|
44
|
+
? record.images
|
|
45
|
+
: [];
|
|
46
|
+
if (!content.trim() && attachments.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const options = record.options && typeof record.options === 'object' && !Array.isArray(record.options)
|
|
50
|
+
? record.options as Record<string, unknown>
|
|
51
|
+
: {};
|
|
52
|
+
return { content, options, attachments };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function sendClaimedQueuedMessage(
|
|
56
|
+
candidate: QueuedSessionMessageRecord,
|
|
57
|
+
runtime: ProviderRuntimeGateway,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const message = readQueuedMessage(candidate.queuedMessage);
|
|
60
|
+
if (!message) {
|
|
61
|
+
sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await runDetachedChatTurn(
|
|
66
|
+
{
|
|
67
|
+
sessionId: candidate.sessionId,
|
|
68
|
+
userId: candidate.userId,
|
|
69
|
+
content: message.content,
|
|
70
|
+
options: { ...message.options, attachments: message.attachments },
|
|
71
|
+
},
|
|
72
|
+
{ runtime },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// The registry check and run reservation are separate operations. If a run
|
|
76
|
+
// wins that tiny race, put the turn back so the next poll tries again.
|
|
77
|
+
if (!result.started && result.error === 'A run was already in progress for this session.') {
|
|
78
|
+
sessionDraftsDb.restoreQueuedMessage(candidate);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Sends every persisted queued turn whose session is currently idle. */
|
|
85
|
+
export async function dispatchQueuedMessages(runtime: ProviderRuntimeGateway): Promise<number> {
|
|
86
|
+
const candidates = sessionDraftsDb.listQueuedMessages();
|
|
87
|
+
let claimed = 0;
|
|
88
|
+
|
|
89
|
+
await Promise.all(candidates.map(async (candidate) => {
|
|
90
|
+
if (chatRunRegistry.isProcessing(candidate.sessionId)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!sessionDraftsDb.claimQueuedMessage(candidate)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
claimed += 1;
|
|
97
|
+
await sendClaimedQueuedMessage(candidate, runtime);
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return claimed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sendClaimedMessage(
|
|
104
|
+
row: ScheduledMessageRow,
|
|
105
|
+
runtime: ProviderRuntimeGateway,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
const result = await runDetachedChatTurn(
|
|
109
|
+
{
|
|
110
|
+
sessionId: row.session_id,
|
|
111
|
+
userId: row.user_id,
|
|
112
|
+
content: row.content,
|
|
113
|
+
options: readOptions(row.options),
|
|
114
|
+
// The user picked this time on purpose; a run that happens to be going
|
|
115
|
+
// is aborted so the scheduled message lands when it was due, instead
|
|
116
|
+
// of being recorded as "not sent — session was busy".
|
|
117
|
+
interruptActiveRun: true,
|
|
118
|
+
},
|
|
119
|
+
{ runtime },
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Recorded rather than retried, and recorded whether the run never started
|
|
123
|
+
// (deleted session, unavailable provider) or started and then failed.
|
|
124
|
+
// Silently dropping a message the user scheduled is worse than telling
|
|
125
|
+
// them it did not go.
|
|
126
|
+
if (!result.started || result.error) {
|
|
127
|
+
scheduledMessagesDb.markFailed(row.id, result.error ?? 'The session was unavailable when this was due.');
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
scheduledMessagesDb.markFailed(row.id, message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sends every message whose time has come.
|
|
137
|
+
*
|
|
138
|
+
* Exported so a test can drive one pass without waiting on the timer.
|
|
139
|
+
*/
|
|
140
|
+
export async function dispatchDueScheduledMessages(
|
|
141
|
+
runtime: ProviderRuntimeGateway,
|
|
142
|
+
now: Date = new Date(),
|
|
143
|
+
): Promise<number> {
|
|
144
|
+
// Claimed before any of them runs, so a long turn cannot let the next poll
|
|
145
|
+
// pick the same message up again.
|
|
146
|
+
const due = scheduledMessagesDb.claimDue(now);
|
|
147
|
+
if (due.length === 0) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Sequentially: a session can only have one run at a time, and two due
|
|
152
|
+
// messages for the same session must not race each other into it.
|
|
153
|
+
for (const row of due) {
|
|
154
|
+
await sendClaimedMessage(row, runtime);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return due.length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Starts the poll that sends scheduled messages.
|
|
162
|
+
*
|
|
163
|
+
* The schedule lives in the database, so a message stays scheduled across a
|
|
164
|
+
* restart and one that came due while the server was down is sent on the first
|
|
165
|
+
* poll after it comes back, rather than being skipped.
|
|
166
|
+
*/
|
|
167
|
+
export function initializeScheduledMessageDispatcher(runtime: ProviderRuntimeGateway): void {
|
|
168
|
+
if (pollTimer) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const poll = () => {
|
|
173
|
+
// A pass that overruns the interval must not be started again underneath
|
|
174
|
+
// itself; the claim is transactional but the runs are not.
|
|
175
|
+
if (dispatchInFlight) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
dispatchInFlight = true;
|
|
179
|
+
void dispatchDueScheduledMessages(runtime)
|
|
180
|
+
.then(() => dispatchQueuedMessages(runtime))
|
|
181
|
+
.catch((error: unknown) => {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
console.error('[ScheduledMessages] Dispatch pass failed', { error: message });
|
|
184
|
+
})
|
|
185
|
+
.finally(() => {
|
|
186
|
+
dispatchInFlight = false;
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
|
191
|
+
// Never keep the process alive just to poll for scheduled messages.
|
|
192
|
+
pollTimer.unref?.();
|
|
193
|
+
|
|
194
|
+
// Catch up on anything that came due while the server was not running.
|
|
195
|
+
poll();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function closeScheduledMessageDispatcher(): void {
|
|
199
|
+
if (pollTimer) {
|
|
200
|
+
clearInterval(pollTimer);
|
|
201
|
+
pollTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|