@vantran-se/openchamber-web 2.1.1 → 2.2.0
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 +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -42,6 +42,7 @@ const createOpenCode = () => {
|
|
|
42
42
|
sent: [],
|
|
43
43
|
switched: [],
|
|
44
44
|
failNext: null,
|
|
45
|
+
failStatus: 500,
|
|
45
46
|
htmlNext: null,
|
|
46
47
|
};
|
|
47
48
|
// v2 wraps `/api/*` payloads in `{ location, data }`; a message page is
|
|
@@ -52,7 +53,7 @@ const createOpenCode = () => {
|
|
|
52
53
|
const method = init.method ?? 'GET';
|
|
53
54
|
if (state.failNext && state.failNext.test(pathname)) {
|
|
54
55
|
state.failNext = null;
|
|
55
|
-
return new Response('boom', { status:
|
|
56
|
+
return new Response('boom', { status: state.failStatus });
|
|
56
57
|
}
|
|
57
58
|
if (method === 'POST' && state.htmlNext?.test(pathname)) {
|
|
58
59
|
state.htmlNext = null;
|
|
@@ -80,7 +81,7 @@ const createOpenCode = () => {
|
|
|
80
81
|
return { state, fetchImpl };
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolveAutoSelection, now } = {}) => {
|
|
84
|
+
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolveAutoSelection, now, beforeScheduledTaskSend, onScheduledTaskResult } = {}) => {
|
|
84
85
|
let eventHandler = () => {};
|
|
85
86
|
let statusHandler = () => {};
|
|
86
87
|
const broadcasts = [];
|
|
@@ -99,6 +100,8 @@ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), k
|
|
|
99
100
|
fetchImpl: openCode.fetchImpl,
|
|
100
101
|
dispatchQuietMs: 0,
|
|
101
102
|
abortHoldMs: 50,
|
|
103
|
+
beforeScheduledTaskSend,
|
|
104
|
+
onScheduledTaskResult,
|
|
102
105
|
};
|
|
103
106
|
if (retryDelayMs) options.retryDelayMs = retryDelayMs;
|
|
104
107
|
if (resolveAutoSelection) options.resolveAutoSelection = resolveAutoSelection;
|
|
@@ -120,6 +123,173 @@ const settle = async (ms = 30) => {
|
|
|
120
123
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
121
124
|
};
|
|
122
125
|
|
|
126
|
+
describe('scheduled task items', () => {
|
|
127
|
+
const scheduled = (projectId = 'project-1', overrides = {}) => item({
|
|
128
|
+
scheduledTask: { projectId, taskId: 'task-1' }, sendConfig: {}, ...overrides,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('deduplicates project and task across sessions, but not across projects', async () => {
|
|
132
|
+
const { runtime, openCode } = createRuntime();
|
|
133
|
+
openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
134
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled());
|
|
135
|
+
await expect(runtime.enqueue('ses_other', DIRECTORY, scheduled())).rejects.toMatchObject({ status: 409 });
|
|
136
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-2'));
|
|
137
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(2);
|
|
138
|
+
runtime.stop();
|
|
139
|
+
await runtime.flush();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('inherits without a model POST and reports sent without waiting for completion', async () => {
|
|
143
|
+
const results = [];
|
|
144
|
+
const { runtime, openCode } = createRuntime({ onScheduledTaskResult: (task, status) => results.push({ task, status }) });
|
|
145
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled());
|
|
146
|
+
await settle();
|
|
147
|
+
expect(openCode.state.switched).toEqual([]);
|
|
148
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
149
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'follow up' });
|
|
150
|
+
expect(runtime.sessionSnapshot(SESSION).items).toEqual([]);
|
|
151
|
+
expect(results).toEqual([{ task: { projectId: 'project-1', taskId: 'task-1' }, status: 'sent' }]);
|
|
152
|
+
runtime.stop();
|
|
153
|
+
await runtime.flush();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('applies explicit selection and keeps the existing directory query', async () => {
|
|
157
|
+
const { runtime, openCode } = createRuntime();
|
|
158
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { providerID: 'openai', modelID: 'chosen', agent: 'plan' } }));
|
|
159
|
+
await settle();
|
|
160
|
+
expect(openCode.state.switched.map((entry) => entry.body)).toEqual([{ model: { providerID: 'openai', id: 'chosen' } }, { agent: 'plan' }]);
|
|
161
|
+
const promptCall = openCode.fetchImpl.mock.calls.find(([url]) => new URL(url).pathname.endsWith('/prompt'));
|
|
162
|
+
expect(new URL(promptCall[0]).searchParams.get('directory')).toBe(DIRECTORY);
|
|
163
|
+
runtime.stop();
|
|
164
|
+
await runtime.flush();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('supports an agent override without switching the target model', async () => {
|
|
168
|
+
const { runtime, openCode } = createRuntime();
|
|
169
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { agent: 'plan' } }));
|
|
170
|
+
await settle();
|
|
171
|
+
expect(openCode.state.switched.map((entry) => entry.body)).toEqual([{ agent: 'plan' }]);
|
|
172
|
+
runtime.stop();
|
|
173
|
+
await runtime.flush();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('keeps an agent-only override across persistence and restart', async () => {
|
|
177
|
+
const first = createRuntime();
|
|
178
|
+
first.openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
179
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { agent: 'plan' } }));
|
|
180
|
+
first.runtime.stop();
|
|
181
|
+
await first.runtime.flush();
|
|
182
|
+
const second = createRuntime({ dataDir: first.dataDir });
|
|
183
|
+
await second.runtime.load();
|
|
184
|
+
expect(second.runtime.sessionSnapshot(SESSION).items[0].sendConfig).toEqual({ agent: 'plan' });
|
|
185
|
+
second.runtime.start();
|
|
186
|
+
await settle();
|
|
187
|
+
expect(second.openCode.state.switched.map((entry) => entry.body)).toEqual([{ agent: 'plan' }]);
|
|
188
|
+
expect(second.openCode.state.sent).toHaveLength(1);
|
|
189
|
+
second.runtime.stop();
|
|
190
|
+
await second.runtime.flush();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it.each([404, 409])('drops a target refusal %s without holding ordinary items behind it', async (statusCode) => {
|
|
194
|
+
const results = [];
|
|
195
|
+
const { runtime, openCode } = createRuntime({
|
|
196
|
+
beforeScheduledTaskSend: async () => { throw Object.assign(new Error('Scheduled target is unavailable'), { statusCode }); },
|
|
197
|
+
onScheduledTaskResult: (_task, status) => results.push(status),
|
|
198
|
+
});
|
|
199
|
+
await runtime.enqueue(SESSION, DIRECTORY, scheduled());
|
|
200
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ text: 'ordinary' }));
|
|
201
|
+
await settle();
|
|
202
|
+
expect(results).toEqual(['failed']);
|
|
203
|
+
expect(openCode.state.sent[0].body.text).toBe('ordinary');
|
|
204
|
+
runtime.stop();
|
|
205
|
+
await runtime.flush();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it.each([
|
|
209
|
+
{ operation: 'prompt', status: 500 },
|
|
210
|
+
{ operation: 'model', status: 500 },
|
|
211
|
+
{ operation: 'model', status: 404 },
|
|
212
|
+
{ operation: 'model', status: 409 },
|
|
213
|
+
{ operation: 'target lookup', status: 503 },
|
|
214
|
+
])('uses the ordinary retry policy for scheduled $operation failure $status', async ({ operation, status }) => {
|
|
215
|
+
const results = [];
|
|
216
|
+
let checks = 0;
|
|
217
|
+
const { runtime, openCode, emit } = createRuntime({
|
|
218
|
+
retryDelayMs: () => 100,
|
|
219
|
+
beforeScheduledTaskSend: async () => {
|
|
220
|
+
checks += 1;
|
|
221
|
+
if (operation === 'target lookup' && checks === 1) throw Object.assign(new Error('upstream unavailable'), { statusCode: 503 });
|
|
222
|
+
},
|
|
223
|
+
onScheduledTaskResult: (_task, status) => results.push(status),
|
|
224
|
+
});
|
|
225
|
+
runtime.start();
|
|
226
|
+
openCode.state.failStatus = status;
|
|
227
|
+
if (operation === 'prompt') openCode.state.failNext = /\/prompt$/;
|
|
228
|
+
if (operation === 'model') openCode.state.failNext = /\/model$/;
|
|
229
|
+
await runtime.enqueue(SESSION, DIRECTORY, operation === 'model'
|
|
230
|
+
? scheduled('project-1', { sendConfig: { providerID: 'openai', modelID: 'chosen' } })
|
|
231
|
+
: scheduled());
|
|
232
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ text: 'ordinary' }));
|
|
233
|
+
await settle();
|
|
234
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(2);
|
|
235
|
+
expect(results).toEqual([]);
|
|
236
|
+
expect(openCode.state.sent).toEqual([]);
|
|
237
|
+
await expect(runtime.enqueue(SESSION, DIRECTORY, scheduled())).rejects.toMatchObject({ status: 409 });
|
|
238
|
+
await settle(130);
|
|
239
|
+
expect(checks).toBe(2);
|
|
240
|
+
expect(results).toEqual(['sent']);
|
|
241
|
+
expect(openCode.state.sent.map((entry) => entry.body.text)).toEqual(['follow up']);
|
|
242
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
243
|
+
await settle();
|
|
244
|
+
expect(openCode.state.sent.map((entry) => entry.body.text)).toEqual(['follow up', 'ordinary']);
|
|
245
|
+
runtime.stop();
|
|
246
|
+
await runtime.flush();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('rejects scheduled takes, leaves them in bulk take, and allows reorder and remove', async () => {
|
|
250
|
+
const results = [];
|
|
251
|
+
const { runtime, openCode } = createRuntime({ onScheduledTaskResult: (task, status) => results.push({ task, status }) });
|
|
252
|
+
openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
253
|
+
const first = await runtime.enqueue(SESSION, DIRECTORY, scheduled());
|
|
254
|
+
const ordinary = await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
255
|
+
const second = await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { scheduledTask: { projectId: 'project-1', taskId: 'task-2' } }));
|
|
256
|
+
await runtime.reorder(SESSION, [second.itemId, ordinary.itemId, first.itemId]);
|
|
257
|
+
await expect(runtime.take(SESSION, first.itemId)).rejects.toMatchObject({ status: 409 });
|
|
258
|
+
expect((await runtime.takeAll(SESSION)).items.map((entry) => entry.id)).toEqual([ordinary.itemId]);
|
|
259
|
+
expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.id)).toEqual([second.itemId, first.itemId]);
|
|
260
|
+
expect(results).toEqual([]);
|
|
261
|
+
await runtime.remove(SESSION, second.itemId);
|
|
262
|
+
expect(results).toEqual([{ task: { projectId: 'project-1', taskId: 'task-2' }, status: 'cancelled' }]);
|
|
263
|
+
runtime.stop();
|
|
264
|
+
await runtime.flush();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('ignores creation-only policy fields on stored scheduled provenance', () => {
|
|
268
|
+
const parsed = parseQueuedItemInput(scheduled('project-1', {
|
|
269
|
+
scheduledTask: { projectId: 'project-1', taskId: 'task-1', goalEnabled: true, goalTokenBudget: 5000, permissionAutoAccept: true },
|
|
270
|
+
}));
|
|
271
|
+
expect(parsed.scheduledTask).toEqual({ projectId: 'project-1', taskId: 'task-1' });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('cancels only the requested task and survives a restart with provenance', async () => {
|
|
275
|
+
const results = [];
|
|
276
|
+
const first = createRuntime({ onScheduledTaskResult: (_task, status) => results.push(status) });
|
|
277
|
+
first.openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
278
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, scheduled());
|
|
279
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, item());
|
|
280
|
+
first.runtime.stop();
|
|
281
|
+
await first.runtime.flush();
|
|
282
|
+
const second = createRuntime({ dataDir: first.dataDir, onScheduledTaskResult: (_task, status) => results.push(status) });
|
|
283
|
+
await second.runtime.load();
|
|
284
|
+
expect(second.runtime.sessionSnapshot(SESSION).items[0].scheduledTask).toEqual({ projectId: 'project-1', taskId: 'task-1' });
|
|
285
|
+
await second.runtime.cancelScheduledTask('project-1', 'task-1');
|
|
286
|
+
expect(second.runtime.sessionSnapshot(SESSION).items).toHaveLength(1);
|
|
287
|
+
expect(results).toEqual(['cancelled']);
|
|
288
|
+
second.runtime.stop();
|
|
289
|
+
await second.runtime.flush();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
123
293
|
describe('auto routing', () => {
|
|
124
294
|
it('switches a queued prompt and a queued command onto the routed model and agent', async () => {
|
|
125
295
|
const resolveAutoSelection = vi.fn(async ({ model }) => (model?.id === 'auto'
|
|
@@ -43,9 +43,8 @@ export const createApnsRuntime = (deps) => {
|
|
|
43
43
|
http2,
|
|
44
44
|
APNS_TOKENS_FILE_PATH,
|
|
45
45
|
readSettingsFromDiskMigrated,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
readSettingsStrict,
|
|
46
|
+
// Host relay identity keys, shared with the private relay (relay/key-store.js).
|
|
47
|
+
relayKeyStore,
|
|
49
48
|
} = deps;
|
|
50
49
|
|
|
51
50
|
let persistLock = Promise.resolve();
|
|
@@ -55,8 +54,8 @@ export const createApnsRuntime = (deps) => {
|
|
|
55
54
|
let warnedUnconfigured = false;
|
|
56
55
|
|
|
57
56
|
// ---------------------------------------------------------------------------
|
|
58
|
-
// Per-server relay signing identity (ECDSA P-256). Auto-generated + persisted
|
|
59
|
-
//
|
|
57
|
+
// Per-server relay signing identity (ECDSA P-256). Auto-generated + persisted by
|
|
58
|
+
// relay/key-store.js. The relay derives serverId = SHA-256(publicKey), verifies
|
|
60
59
|
// each request's signature, and only delivers to tokens this server registered — so a leaked
|
|
61
60
|
// device token alone can't be used to push. Zero-config: the keypair generates on first use.
|
|
62
61
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +64,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
65
64
|
// relay identity — same keypair, same storage, same serverId derivation).
|
|
66
65
|
const getOrCreateRelayKeypair = async () => {
|
|
67
66
|
if (cachedRelayKey) return cachedRelayKey;
|
|
68
|
-
cachedRelayKey = await getOrCreateRelaySigningKeypair({ crypto,
|
|
67
|
+
cachedRelayKey = await getOrCreateRelaySigningKeypair({ crypto, relayKeyStore });
|
|
69
68
|
return cachedRelayKey;
|
|
70
69
|
};
|
|
71
70
|
|
|
@@ -28,9 +28,25 @@ const createMemoryFs = () => {
|
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// In-memory relay key store: the auto-generated signing keypair persists and
|
|
32
|
+
// reads back; `generations` counts how many keypairs were minted.
|
|
33
|
+
const makeMemoryKeyStore = () => {
|
|
34
|
+
const slots = {};
|
|
35
|
+
const store = {
|
|
36
|
+
generations: 0,
|
|
37
|
+
getOrCreate: async (slot, generate) => {
|
|
38
|
+
if (!slots[slot]) {
|
|
39
|
+
store.generations += 1;
|
|
40
|
+
slots[slot] = await generate();
|
|
41
|
+
}
|
|
42
|
+
return slots[slot];
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return store;
|
|
46
|
+
};
|
|
47
|
+
|
|
31
48
|
const makeDeps = (overrides = {}) => {
|
|
32
|
-
|
|
33
|
-
let settings = {};
|
|
49
|
+
const settings = {};
|
|
34
50
|
return {
|
|
35
51
|
fsPromises: createMemoryFs(),
|
|
36
52
|
path: { dirname: () => '/tmp' },
|
|
@@ -38,7 +54,7 @@ const makeDeps = (overrides = {}) => {
|
|
|
38
54
|
http2: { connect: vi.fn(() => { throw new Error('http2 must not be used in relay mode'); }) },
|
|
39
55
|
APNS_TOKENS_FILE_PATH: '/tmp/apns-tokens.json',
|
|
40
56
|
readSettingsFromDiskMigrated: vi.fn(async () => settings),
|
|
41
|
-
|
|
57
|
+
relayKeyStore: makeMemoryKeyStore(),
|
|
42
58
|
...overrides,
|
|
43
59
|
};
|
|
44
60
|
};
|
|
@@ -144,7 +160,7 @@ describe('apns runtime relay mode (default)', () => {
|
|
|
144
160
|
expect(keys.length).toBeGreaterThanOrEqual(2);
|
|
145
161
|
expect(keys.every((k) => k.x === keys[0].x && k.y === keys[0].y)).toBe(true);
|
|
146
162
|
// Keypair was generated + persisted exactly once.
|
|
147
|
-
expect(deps.
|
|
163
|
+
expect(deps.relayKeyStore.generations).toBe(1);
|
|
148
164
|
});
|
|
149
165
|
|
|
150
166
|
it('honors an explicit sandbox environment override for every token', async () => {
|
|
@@ -13,8 +13,18 @@ other.
|
|
|
13
13
|
scheduled-task action allowlist. `actions.js` marks CLI-only actions with
|
|
14
14
|
`agentExposed: false` (currently `schedule.status`); the agent tool consumes
|
|
15
15
|
the filtered `OPENCHAMBER_AGENT_TOOL_*` exports. `schedule.toggle` requires
|
|
16
|
-
the `disabled` boolean and replaces separate enable/disable actions;
|
|
16
|
+
the `disabled` boolean and replaces separate enable/disable actions; its
|
|
17
|
+
`enabled` is the saved state, never the requested one. Loop tasks and the
|
|
18
|
+
refusal to enable a repository loop are the scheduled-task service's rules
|
|
19
|
+
(`../scheduled-tasks/DOCUMENTATION.md`, *Local approval*).
|
|
17
20
|
`schedule.list` also returns scheduler status as `scheduler`.
|
|
21
|
+
`schedule.update` patches a task in place: only the fields the call names
|
|
22
|
+
change, the id and run state stay, and a task driven by a loop file is
|
|
23
|
+
refused (the file is its source).
|
|
24
|
+
`schedule.create` and `schedule.update` accept `targetSessionId`, validated
|
|
25
|
+
by the scheduled-task service. An empty update clears the target. Create
|
|
26
|
+
without a model inherits the target selection; an explicit agent overrides
|
|
27
|
+
it. A task without a target still requires a model.
|
|
18
28
|
- `routes.js` is the authenticated CLI HTTP adapter. It forwards one action,
|
|
19
29
|
preserves service status and partial-result details, and propagates request
|
|
20
30
|
cancellation.
|
|
@@ -53,6 +63,14 @@ other.
|
|
|
53
63
|
- Send and fork dispatches without an explicit model/agent/variant reuse the
|
|
54
64
|
target session's last user-message selection before falling back to the
|
|
55
65
|
configured defaults; only session creation resolves defaults directly.
|
|
66
|
+
- `session.create` with `pullRequest` passes the number, and `worktree` and
|
|
67
|
+
`branch` as folder and local-branch names, to the session service, which
|
|
68
|
+
creates the worktree through the injected `createChangeRequestWorktree`
|
|
69
|
+
(the Git routes' contributor pipeline) before any session exists. A
|
|
70
|
+
malformed number, or `startRef`/`setUpstream` beside it, is a 400; a
|
|
71
|
+
server without that pipeline answers 501 and never makes a plain branch
|
|
72
|
+
worktree instead; a provider or transfer refusal keeps its status and
|
|
73
|
+
message, and no session is created.
|
|
56
74
|
- Default agents resolve from the owning project before global settings and
|
|
57
75
|
OpenCode defaults. Directory-based requests identify the project before
|
|
58
76
|
creating a worktree; existing linked worktrees resolve through Git's primary
|
|
@@ -19,7 +19,8 @@ export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
|
|
|
19
19
|
{ action: 'session.link', title: 'Link a change or issue to a session', description: 'Link a change under review (pull or merge request) or an issue, on any service, to a session; requires link, sessionId defaults to the current session. Each link is added beside the others; linking one that is already there updates its title instead of adding it twice. Links are only added; the user removes them' },
|
|
20
20
|
{ action: 'schedule.status', title: 'Check scheduler status', description: 'Check scheduler status; no parameters', agentExposed: false },
|
|
21
21
|
{ action: 'schedule.list', title: 'List scheduled tasks', description: 'List tasks and scheduler status; scope with projectId or directory' },
|
|
22
|
-
{ action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt,
|
|
22
|
+
{ action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt, one schedule selector, and model unless targetSessionId is set. targetSessionId reuses a session; without model it keeps that session selection' },
|
|
23
|
+
{ action: 'schedule.update', title: 'Change a scheduled task', description: 'Change taskId in place, keeping its id and run state; pass only what changes: name, prompt, model, variant, agent, targetSessionId (empty clears it), a schedule selector, timezone, disabled' },
|
|
23
24
|
{ action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
|
|
24
25
|
{ action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
|
|
25
26
|
{ action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { OpenCode } from '@opencode/client';
|
|
3
4
|
import { OpenChamberControlError, asControlError } from './error.js';
|
|
@@ -10,17 +11,12 @@ const WAIT_POLL_INTERVAL_MS = 500;
|
|
|
10
11
|
// One service, both capabilities: which tool asked is the caller's concern.
|
|
11
12
|
const CONTROL_ACTIONS = new Set(OPENCHAMBER_ALL_ACTIONS);
|
|
12
13
|
const SCHEDULE_TASK_ID_ACTIONS = new Set([
|
|
14
|
+
'schedule.update',
|
|
13
15
|
'schedule.run',
|
|
14
16
|
'schedule.delete',
|
|
15
17
|
'schedule.toggle',
|
|
16
18
|
]);
|
|
17
19
|
|
|
18
|
-
const asNonEmptyString = (value) => {
|
|
19
|
-
if (typeof value !== 'string') return null;
|
|
20
|
-
const trimmed = value.trim();
|
|
21
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
20
|
const positiveInteger = (value, fallback, field) => {
|
|
25
21
|
if (value === undefined || value === null) return fallback;
|
|
26
22
|
const number = Number(value);
|
|
@@ -120,7 +116,8 @@ const buildScheduledTask = (input) => {
|
|
|
120
116
|
const prompt = asNonEmptyString(input.prompt);
|
|
121
117
|
if (!name) throw new OpenChamberControlError('name is required', 400);
|
|
122
118
|
if (!prompt) throw new OpenChamberControlError('prompt is required', 400);
|
|
123
|
-
const
|
|
119
|
+
const targetSessionId = asNonEmptyString(input.targetSessionId);
|
|
120
|
+
const model = targetSessionId && input.model === undefined ? { useDefaults: !asNonEmptyString(input.agent) } : parseModel(input.model);
|
|
124
121
|
const goalTokenBudget = input.goalTokenBudget;
|
|
125
122
|
if (goalTokenBudget !== undefined && input.goal !== true) {
|
|
126
123
|
throw new OpenChamberControlError('goalTokenBudget requires goal', 400);
|
|
@@ -128,7 +125,7 @@ const buildScheduledTask = (input) => {
|
|
|
128
125
|
if (goalTokenBudget !== undefined && (!Number.isSafeInteger(goalTokenBudget) || goalTokenBudget < 1000 || goalTokenBudget > 100_000_000)) {
|
|
129
126
|
throw new OpenChamberControlError('goalTokenBudget must be from 1000 to 100000000', 400);
|
|
130
127
|
}
|
|
131
|
-
|
|
128
|
+
const task = {
|
|
132
129
|
name,
|
|
133
130
|
enabled: input.disabled !== true,
|
|
134
131
|
schedule: buildSchedule(input),
|
|
@@ -141,6 +138,70 @@ const buildScheduledTask = (input) => {
|
|
|
141
138
|
...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
|
|
142
139
|
},
|
|
143
140
|
};
|
|
141
|
+
if (input.targetSessionId !== undefined) task.targetSessionId = targetSessionId ?? input.targetSessionId;
|
|
142
|
+
return task;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `schedule.update`: the stored task with only the fields the call names
|
|
147
|
+
* changed. The id and the run state stay, so history and references survive;
|
|
148
|
+
* the scheduler re-arms from the new schedule on save.
|
|
149
|
+
*/
|
|
150
|
+
const patchScheduledTask = (existing, input) => {
|
|
151
|
+
if (existing.loopFile) {
|
|
152
|
+
throw new OpenChamberControlError(`This task comes from ${existing.loopFile}; change that file instead`, 409);
|
|
153
|
+
}
|
|
154
|
+
const execution = { ...existing.execution };
|
|
155
|
+
if (input.prompt !== undefined) {
|
|
156
|
+
const prompt = asNonEmptyString(input.prompt);
|
|
157
|
+
if (!prompt) throw new OpenChamberControlError('prompt cannot be empty', 400);
|
|
158
|
+
execution.prompt = prompt;
|
|
159
|
+
}
|
|
160
|
+
if (input.model !== undefined) {
|
|
161
|
+
Object.assign(execution, parseModel(input.model));
|
|
162
|
+
// A thinking level belongs to the model it was chosen for.
|
|
163
|
+
delete execution.variant;
|
|
164
|
+
delete execution.useDefaults;
|
|
165
|
+
}
|
|
166
|
+
for (const field of ['variant', 'agent']) {
|
|
167
|
+
if (input[field] === undefined) continue;
|
|
168
|
+
const value = asNonEmptyString(input[field]);
|
|
169
|
+
if (value) execution[field] = value;
|
|
170
|
+
else delete execution[field];
|
|
171
|
+
if (field === 'agent' && value && (input.targetSessionId || existing.targetSessionId)) delete execution.useDefaults;
|
|
172
|
+
}
|
|
173
|
+
if (typeof input.goal === 'boolean') {
|
|
174
|
+
if (input.goal) execution.goalEnabled = true;
|
|
175
|
+
else {
|
|
176
|
+
delete execution.goalEnabled;
|
|
177
|
+
delete execution.goalTokenBudget;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (input.goalTokenBudget !== undefined) {
|
|
181
|
+
if (!execution.goalEnabled) throw new OpenChamberControlError('goalTokenBudget requires goal', 400);
|
|
182
|
+
if (!Number.isSafeInteger(input.goalTokenBudget) || input.goalTokenBudget < 1000 || input.goalTokenBudget > 100_000_000) {
|
|
183
|
+
throw new OpenChamberControlError('goalTokenBudget must be from 1000 to 100000000', 400);
|
|
184
|
+
}
|
|
185
|
+
execution.goalTokenBudget = input.goalTokenBudget;
|
|
186
|
+
}
|
|
187
|
+
let name = existing.name;
|
|
188
|
+
if (input.name !== undefined) {
|
|
189
|
+
name = asNonEmptyString(input.name);
|
|
190
|
+
if (!name) throw new OpenChamberControlError('name cannot be empty', 400);
|
|
191
|
+
}
|
|
192
|
+
const replacesSchedule = ['daily', 'weekly', 'once', 'cron'].some((key) => asNonEmptyString(input[key]));
|
|
193
|
+
const timezone = asNonEmptyString(input.timezone);
|
|
194
|
+
const schedule = replacesSchedule
|
|
195
|
+
? buildSchedule(input)
|
|
196
|
+
: (timezone ? { ...existing.schedule, timezone } : existing.schedule);
|
|
197
|
+
return {
|
|
198
|
+
id: existing.id,
|
|
199
|
+
targetSessionId: input.targetSessionId === undefined ? existing.targetSessionId : input.targetSessionId,
|
|
200
|
+
name,
|
|
201
|
+
enabled: typeof input.disabled === 'boolean' ? !input.disabled : existing.enabled,
|
|
202
|
+
schedule,
|
|
203
|
+
execution,
|
|
204
|
+
};
|
|
144
205
|
};
|
|
145
206
|
|
|
146
207
|
export const createOpenChamberControlService = (dependencies) => {
|
|
@@ -262,18 +323,36 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
262
323
|
return asPublicStatus(statuses[sessionID]);
|
|
263
324
|
};
|
|
264
325
|
|
|
265
|
-
// `order: 'desc'` puts the newest
|
|
266
|
-
//
|
|
326
|
+
// The message list is cursor-paginated and `order: 'desc'` puts the newest
|
|
327
|
+
// messages in the page, so a single call returns only the recent tail.
|
|
328
|
+
// `all: true` walks `cursor.next` until the history is exhausted; a bounded
|
|
329
|
+
// `limit` stops once enough text messages are collected. Later pages carry
|
|
330
|
+
// the order inside the cursor, and OpenCode rejects a cursor combined with
|
|
331
|
+
// `order`, so only the first page sends it. An empty page or a repeated
|
|
332
|
+
// cursor ends the walk. `extractTextMessages` sorts the gathered records
|
|
333
|
+
// oldest-first for the caller.
|
|
334
|
+
const MESSAGE_PAGE_LIMIT = 200;
|
|
267
335
|
const sessionMessages = async (client, sessionID, role, limit) => {
|
|
268
336
|
const fetchLimit = limit === undefined ? undefined : Math.max(100, limit * 4);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let
|
|
272
|
-
|
|
273
|
-
response = await client.message.list({
|
|
274
|
-
|
|
275
|
-
|
|
337
|
+
const collected = [];
|
|
338
|
+
const seenCursors = new Set();
|
|
339
|
+
let cursor;
|
|
340
|
+
for (;;) {
|
|
341
|
+
const response = await client.message.list({
|
|
342
|
+
sessionID,
|
|
343
|
+
limit: fetchLimit ?? MESSAGE_PAGE_LIMIT,
|
|
344
|
+
...(cursor ? { cursor } : fetchLimit ? { order: 'desc' } : {}),
|
|
345
|
+
});
|
|
346
|
+
const raw = Array.isArray(response?.data) ? response.data : [];
|
|
347
|
+
collected.push(...raw);
|
|
348
|
+
const next = asNonEmptyString(response?.cursor?.next);
|
|
349
|
+
if (!next || raw.length === 0 || seenCursors.has(next)) break;
|
|
350
|
+
// A bounded read only needs enough pages to project `limit` text messages.
|
|
351
|
+
if (limit !== undefined && extractTextMessages(collected, role).length >= limit) break;
|
|
352
|
+
seenCursors.add(next);
|
|
353
|
+
cursor = next;
|
|
276
354
|
}
|
|
355
|
+
const messages = extractTextMessages(collected, role);
|
|
277
356
|
return limit === undefined ? messages : messages.slice(-limit);
|
|
278
357
|
};
|
|
279
358
|
|
|
@@ -368,6 +447,10 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
368
447
|
if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
|
|
369
448
|
if (input.lastAssistant === true && input.wait !== true) throw new OpenChamberControlError('lastAssistant requires wait', 400);
|
|
370
449
|
assertSingleScope(input);
|
|
450
|
+
// Only a new session gets a worktree; elsewhere the number would be ignored.
|
|
451
|
+
if (input.pullRequest !== undefined && action !== 'session.create') {
|
|
452
|
+
throw new OpenChamberControlError('pullRequest applies only to session.create', 400);
|
|
453
|
+
}
|
|
371
454
|
const sessionID = asNonEmptyString(input.sessionId);
|
|
372
455
|
const parentSessionID = asNonEmptyString(contextSessionId);
|
|
373
456
|
const returnResult = input.returnResult === true;
|
|
@@ -395,6 +478,16 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
395
478
|
...(typeof input.setUpstream === 'boolean' ? { setUpstream: input.setUpstream } : {}),
|
|
396
479
|
...(asNonEmptyString(input.messageId) ? { messageId: input.messageId.trim() } : {}),
|
|
397
480
|
};
|
|
481
|
+
// A pull request makes a worktree of its own, so its branch name (and a
|
|
482
|
+
// start ref the session service refuses) travel without a worktree name.
|
|
483
|
+
if (input.pullRequest !== undefined) {
|
|
484
|
+
payload.pullRequest = input.pullRequest;
|
|
485
|
+
if (!payload.worktree) {
|
|
486
|
+
payload.worktree = {};
|
|
487
|
+
if (asNonEmptyString(input.branch)) payload.worktree.branchName = input.branch.trim();
|
|
488
|
+
if (asNonEmptyString(input.startRef)) payload.worktree.startRef = input.startRef.trim();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
398
491
|
const startedAt = now();
|
|
399
492
|
let result;
|
|
400
493
|
if (action === 'session.create') {
|
|
@@ -488,7 +581,6 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
488
581
|
parameters.url = parsed.toString();
|
|
489
582
|
}
|
|
490
583
|
|
|
491
|
-
|
|
492
584
|
if (action === 'browser.click') {
|
|
493
585
|
const selector = asNonEmptyString(input.selector);
|
|
494
586
|
const text = asNonEmptyString(input.text);
|
|
@@ -648,6 +740,12 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
648
740
|
const result = await scheduledTaskService.upsert(projectID, buildScheduledTask(input));
|
|
649
741
|
return { task: result.task, created: result.created };
|
|
650
742
|
}
|
|
743
|
+
case 'schedule.update': {
|
|
744
|
+
const existing = (await scheduledTaskService.list(projectID)).find((task) => task.id === taskID);
|
|
745
|
+
if (!existing) throw new OpenChamberControlError(`Scheduled task not found: ${taskID}`, 404);
|
|
746
|
+
const result = await scheduledTaskService.upsert(projectID, patchScheduledTask(existing, input));
|
|
747
|
+
return { task: result.task, updated: true };
|
|
748
|
+
}
|
|
651
749
|
case 'schedule.run':
|
|
652
750
|
return scheduledTaskService.run(projectID, taskID);
|
|
653
751
|
case 'schedule.delete':
|
|
@@ -656,8 +754,9 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
656
754
|
if (typeof input.disabled !== 'boolean') {
|
|
657
755
|
throw new OpenChamberControlError('disabled is required for schedule.toggle', 400);
|
|
658
756
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
757
|
+
const task = await scheduledTaskService.setEnabled(projectID, taskID, input.disabled === false);
|
|
758
|
+
// What was saved, which is what the scheduler acts on.
|
|
759
|
+
return { task, enabled: task?.enabled === true };
|
|
661
760
|
}
|
|
662
761
|
}
|
|
663
762
|
}
|