@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import fsPromises from 'node:fs/promises';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
|
|
@@ -284,8 +285,57 @@ describe('todos', () => {
|
|
|
284
285
|
});
|
|
285
286
|
|
|
286
287
|
test('clamps oversized todo text', async () => {
|
|
287
|
-
await runtime.saveTodos(PROJECT_ID, [{ id: 't1', text: 'z'.repeat(
|
|
288
|
-
expect((await runtime.readContext(PROJECT_ID)).todos[0].text).toHaveLength(
|
|
288
|
+
await runtime.saveTodos(PROJECT_ID, [{ id: 't1', text: 'z'.repeat(1500), createdAt: 1 }]);
|
|
289
|
+
expect((await runtime.readContext(PROJECT_ID)).todos[0].text).toHaveLength(1000);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('committed changes', () => {
|
|
294
|
+
test('announces the owner only after note and todo changes reach disk', async () => {
|
|
295
|
+
const changes = [];
|
|
296
|
+
const announcing = createProjectContextRuntime({
|
|
297
|
+
fsPromises,
|
|
298
|
+
path,
|
|
299
|
+
projectsDirPath,
|
|
300
|
+
createId: () => `note-${++idCounter}`,
|
|
301
|
+
onChanged: (projectId) => {
|
|
302
|
+
changes.push({ projectId, context: JSON.parse(readFileSync(announcing.contextPathFor(projectId), 'utf8')) });
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
await announcing.readContext(PROJECT_ID);
|
|
306
|
+
expect(changes).toEqual([]);
|
|
307
|
+
const { note } = await announcing.createNote(PROJECT_ID, { body: 'peer note' });
|
|
308
|
+
expect(changes.map(change => change.projectId)).toEqual([PROJECT_ID]);
|
|
309
|
+
expect(changes[0].context.notes[0].body).toBe('peer note');
|
|
310
|
+
|
|
311
|
+
await announcing.updateNote(PROJECT_ID, note.id, { body: 'peer edit' });
|
|
312
|
+
await announcing.saveTodos(PROJECT_ID, [{ id: 't1', text: 'peer todo', completed: true, createdAt: 1 }]);
|
|
313
|
+
expect(changes.map(change => change.projectId)).toEqual([PROJECT_ID, PROJECT_ID, PROJECT_ID]);
|
|
314
|
+
expect(changes[1].context.notes[0].body).toBe('peer edit');
|
|
315
|
+
expect(changes[2].context.todos[0].completed).toBe(true);
|
|
316
|
+
|
|
317
|
+
await announcing.deleteNote(PROJECT_ID, note.id);
|
|
318
|
+
expect(changes.at(-1).context.notes).toEqual([]);
|
|
319
|
+
const count = changes.length;
|
|
320
|
+
await announcing.deleteNote(PROJECT_ID, 'missing');
|
|
321
|
+
await expect(announcing.createNote(PROJECT_ID, { body: ' ' })).rejects.toThrow('body is required');
|
|
322
|
+
expect(changes).toHaveLength(count);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('a failed atomic write does not announce or replace the last committed context', async () => {
|
|
326
|
+
await runtime.createNote(PROJECT_ID, { body: 'kept' });
|
|
327
|
+
const changes = [];
|
|
328
|
+
const failing = createProjectContextRuntime({
|
|
329
|
+
fsPromises: { ...fsPromises, rename: async () => { throw new Error('disk full'); } },
|
|
330
|
+
path,
|
|
331
|
+
projectsDirPath,
|
|
332
|
+
onChanged: (projectId) => changes.push(projectId),
|
|
333
|
+
});
|
|
334
|
+
await expect(failing.saveTodos(PROJECT_ID, [{ id: 't1', text: 'not saved' }])).rejects.toThrow('disk full');
|
|
335
|
+
expect(changes).toEqual([]);
|
|
336
|
+
const context = await runtime.readContext(PROJECT_ID);
|
|
337
|
+
expect(context.notes[0].body).toBe('kept');
|
|
338
|
+
expect(context.todos).toEqual([]);
|
|
289
339
|
});
|
|
290
340
|
});
|
|
291
341
|
|
|
@@ -404,6 +454,8 @@ describe('plans', () => {
|
|
|
404
454
|
expect(read.title).toBe('My Plan');
|
|
405
455
|
expect(read.body).toBe('step one');
|
|
406
456
|
expect(read.raw).toBe('# My Plan\n\nstep one');
|
|
457
|
+
// Where the file lives, so a comment on the plan can point an agent at it.
|
|
458
|
+
expect(read.path).toBe(path.join(plansDir(), plan.file));
|
|
407
459
|
});
|
|
408
460
|
|
|
409
461
|
test('newest plan is listed first', async () => {
|
|
@@ -567,6 +619,7 @@ describe('shared plans', () => {
|
|
|
567
619
|
const read = await sharedRuntime.readPlan(PROJECT_ID, 'shared:roadmap.md');
|
|
568
620
|
expect(read.title).toBe('Roadmap');
|
|
569
621
|
expect(read.raw).toBe('# Roadmap\n\n- ship it\n');
|
|
622
|
+
expect(read.path).toBe(path.join(sharedDir, 'roadmap.md'));
|
|
570
623
|
expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:missing.md')).toBeNull();
|
|
571
624
|
expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:../escape.md')).toBeNull();
|
|
572
625
|
});
|
|
@@ -592,6 +645,29 @@ describe('shared plans', () => {
|
|
|
592
645
|
expect((await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md')).deleted).toBe(false);
|
|
593
646
|
});
|
|
594
647
|
|
|
648
|
+
test('announces shared plan updates and deletes, and stays quiet for missing files', async () => {
|
|
649
|
+
const changes = [];
|
|
650
|
+
const notifyingRuntime = createProjectContextRuntime({
|
|
651
|
+
fsPromises,
|
|
652
|
+
path,
|
|
653
|
+
projectsDirPath,
|
|
654
|
+
createId: () => `plan-${++idCounter}`,
|
|
655
|
+
resolveSharedPlansDir: async () => sharedDir,
|
|
656
|
+
onChanged: (projectId) => changes.push(projectId),
|
|
657
|
+
});
|
|
658
|
+
await fsPromises.mkdir(sharedDir, { recursive: true });
|
|
659
|
+
await fsPromises.writeFile(path.join(sharedDir, 'a.md'), '# A\n');
|
|
660
|
+
|
|
661
|
+
await notifyingRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: '# Renamed\n' });
|
|
662
|
+
expect(changes).toEqual([PROJECT_ID]);
|
|
663
|
+
await notifyingRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
|
|
664
|
+
expect(changes).toEqual([PROJECT_ID, PROJECT_ID]);
|
|
665
|
+
|
|
666
|
+
await notifyingRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: 'x' });
|
|
667
|
+
await notifyingRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
|
|
668
|
+
expect(changes).toEqual([PROJECT_ID, PROJECT_ID]);
|
|
669
|
+
});
|
|
670
|
+
|
|
595
671
|
test('share moves a personal plan into the shared folder and unshare brings it back, avoiding name collisions', async () => {
|
|
596
672
|
const { plan } = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'body' });
|
|
597
673
|
const shared = await sharedRuntime.sharePlan(PROJECT_ID, plan.id);
|
|
@@ -38,7 +38,7 @@ const clampLength = (value, maxLength) => {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
const normalizeStatus = (value) => {
|
|
41
|
-
if (value === 'running' || value === 'success' || value === 'error' || value === 'idle') {
|
|
41
|
+
if (value === 'running' || value === 'success' || value === 'error' || value === 'idle' || value === 'queued' || value === 'sent' || value === 'skipped' || value === 'failed' || value === 'cancelled') {
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
44
|
return 'idle';
|
|
@@ -218,7 +218,7 @@ const normalizeSchedule = (value, existingSchedule) => {
|
|
|
218
218
|
return { kind, cron, timezone };
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
-
const normalizeExecution = (value) => {
|
|
221
|
+
const normalizeExecution = (value, targetSessionId) => {
|
|
222
222
|
if (!value || typeof value !== 'object') {
|
|
223
223
|
throw new Error('execution is required');
|
|
224
224
|
}
|
|
@@ -230,6 +230,9 @@ const normalizeExecution = (value) => {
|
|
|
230
230
|
const agent = asNonEmptyString(value.agent);
|
|
231
231
|
const goalEnabled = value.goalEnabled === true;
|
|
232
232
|
const permissionAutoAccept = value.permissionAutoAccept === true;
|
|
233
|
+
// Model, thinking level and agent come from the session defaults at run
|
|
234
|
+
// time; the stored ones are kept only for when this is turned off.
|
|
235
|
+
const useDefaults = value.useDefaults === true;
|
|
233
236
|
const goalTokenBudget = typeof value.goalTokenBudget === 'number'
|
|
234
237
|
&& Number.isFinite(value.goalTokenBudget)
|
|
235
238
|
&& value.goalTokenBudget > 0
|
|
@@ -239,17 +242,18 @@ const normalizeExecution = (value) => {
|
|
|
239
242
|
if (!prompt) {
|
|
240
243
|
throw new Error('execution.prompt is required');
|
|
241
244
|
}
|
|
242
|
-
if (!providerID) {
|
|
245
|
+
if (!useDefaults && !targetSessionId && !providerID) {
|
|
243
246
|
throw new Error('execution.providerID is required');
|
|
244
247
|
}
|
|
245
|
-
if (!modelID) {
|
|
248
|
+
if (!useDefaults && !targetSessionId && !modelID) {
|
|
246
249
|
throw new Error('execution.modelID is required');
|
|
247
250
|
}
|
|
251
|
+
if (targetSessionId && Boolean(providerID) !== Boolean(modelID)) throw new Error('execution providerID and modelID are required together');
|
|
248
252
|
|
|
249
253
|
return {
|
|
250
254
|
prompt,
|
|
251
|
-
providerID,
|
|
252
|
-
|
|
255
|
+
...(providerID && modelID ? { providerID, modelID } : {}),
|
|
256
|
+
...(useDefaults ? { useDefaults: true } : {}),
|
|
253
257
|
...(variant ? { variant } : {}),
|
|
254
258
|
...(agent ? { agent } : {}),
|
|
255
259
|
...(goalEnabled ? { goalEnabled: true } : {}),
|
|
@@ -333,7 +337,10 @@ const normalizeTaskForStorage = (value, options) => {
|
|
|
333
337
|
: (existingTask?.enabled ?? true);
|
|
334
338
|
|
|
335
339
|
const schedule = normalizeSchedule(value.schedule, existingTask?.schedule);
|
|
336
|
-
const
|
|
340
|
+
const targetSessionId = asNonEmptyString(value.targetSessionId);
|
|
341
|
+
if (value.targetSessionId != null && value.targetSessionId !== '' && !targetSessionId) throw new Error('targetSessionId is invalid');
|
|
342
|
+
if (targetSessionId && !/^[A-Za-z0-9_-]{4,128}$/.test(targetSessionId)) throw new Error('targetSessionId is invalid');
|
|
343
|
+
const execution = normalizeExecution(value.execution, targetSessionId);
|
|
337
344
|
|
|
338
345
|
// Loop provenance: absolute path of the `.agents/loops/*.md` file driving
|
|
339
346
|
// this task, when any. Preserved on every write so the scheduler can detect
|
|
@@ -348,7 +355,7 @@ const normalizeTaskForStorage = (value, options) => {
|
|
|
348
355
|
updatedAt: refreshUpdatedAt ? nowMs : baseState.updatedAt ?? nowMs,
|
|
349
356
|
};
|
|
350
357
|
|
|
351
|
-
|
|
358
|
+
const storedTask = {
|
|
352
359
|
id,
|
|
353
360
|
name,
|
|
354
361
|
enabled,
|
|
@@ -357,6 +364,8 @@ const normalizeTaskForStorage = (value, options) => {
|
|
|
357
364
|
state,
|
|
358
365
|
...(loopFile ? { loopFile } : {}),
|
|
359
366
|
};
|
|
367
|
+
if (targetSessionId) storedTask.targetSessionId = targetSessionId;
|
|
368
|
+
return storedTask;
|
|
360
369
|
};
|
|
361
370
|
|
|
362
371
|
// `loopApprovals`: loop file path -> fingerprint of the version the user
|
|
@@ -865,12 +874,16 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
865
874
|
// A project loop comes with the repository: `enabled: true` in its file
|
|
866
875
|
// is the author's suggestion. It runs here only once the user enabled
|
|
867
876
|
// this version of it on this machine (see setLoopApproval); a change to
|
|
868
|
-
// what it runs or when needs a new approval.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
877
|
+
// what it runs or when needs a new approval. The returned task says
|
|
878
|
+
// which of the two held it back (`loopApproval`); it is never stored.
|
|
879
|
+
const heldBack = new Map();
|
|
880
|
+
loops = loops.map((loop) => {
|
|
881
|
+
if (loop?.scope !== 'project' || loop.definition?.enabled !== true || approvals[loop.filePath] === loop.fingerprint) {
|
|
882
|
+
return loop;
|
|
883
|
+
}
|
|
884
|
+
heldBack.set(loop.filePath, approvals[loop.filePath] ? 'outdated' : 'required');
|
|
885
|
+
return { ...loop, definition: { ...loop.definition, enabled: false } };
|
|
886
|
+
});
|
|
874
887
|
|
|
875
888
|
const activeLoopFilePaths = new Set();
|
|
876
889
|
const pendingLoops = new Map();
|
|
@@ -967,7 +980,9 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
967
980
|
scheduledTasks: toStoredTasks(current, nextTasks, { replacedIDs }),
|
|
968
981
|
});
|
|
969
982
|
|
|
970
|
-
return nextTasks
|
|
983
|
+
return nextTasks.map((task) => (
|
|
984
|
+
task.loopFile && heldBack.has(task.loopFile) ? { ...task, loopApproval: heldBack.get(task.loopFile) } : task
|
|
985
|
+
));
|
|
971
986
|
});
|
|
972
987
|
};
|
|
973
988
|
|
|
@@ -23,6 +23,19 @@ const createRuntime = async () => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
describe('project-config runtime', () => {
|
|
26
|
+
it('round-trips target tasks, terminal states and agent-only overrides without changing legacy model requirements', async () => {
|
|
27
|
+
const { runtime, cleanup } = await createRuntime();
|
|
28
|
+
try {
|
|
29
|
+
const input = { name: 'Reuse', enabled: true, targetSessionId: 'ses_target', schedule: { kind: 'daily', time: '09:00' }, execution: { prompt: 'Continue', agent: 'plan' } };
|
|
30
|
+
const saved = await runtime.upsertScheduledTask('project-test', input);
|
|
31
|
+
for (const lastStatus of ['queued', 'sent', 'skipped', 'failed', 'cancelled']) {
|
|
32
|
+
await runtime.updateScheduledTaskState('project-test', saved.task.id, { lastStatus, lastSessionId: 'ses_target' });
|
|
33
|
+
expect((await runtime.listScheduledTasks('project-test'))[0]).toMatchObject({ targetSessionId: 'ses_target', state: { lastStatus, lastSessionId: 'ses_target' } });
|
|
34
|
+
}
|
|
35
|
+
await expect(runtime.upsertScheduledTask('project-test', { ...input, targetSessionId: '../bad' })).rejects.toThrow('targetSessionId is invalid');
|
|
36
|
+
await expect(runtime.upsertScheduledTask('project-test', { ...input, targetSessionId: '' })).rejects.toThrow('execution.providerID is required');
|
|
37
|
+
} finally { await cleanup(); }
|
|
38
|
+
});
|
|
26
39
|
it('creates and persists a scheduled task', async () => {
|
|
27
40
|
const { runtime, cleanup } = await createRuntime();
|
|
28
41
|
try {
|
|
@@ -53,6 +66,23 @@ describe('project-config runtime', () => {
|
|
|
53
66
|
}
|
|
54
67
|
});
|
|
55
68
|
|
|
69
|
+
it('accepts a task that follows the session defaults without a model, and still needs one when pinned', async () => {
|
|
70
|
+
const { runtime, cleanup } = await createRuntime();
|
|
71
|
+
const task = (execution) => ({
|
|
72
|
+
name: 'Follow defaults',
|
|
73
|
+
enabled: true,
|
|
74
|
+
schedule: { kind: 'daily', time: '09:30', timezone: 'UTC' },
|
|
75
|
+
execution: { prompt: 'Summarize', ...execution },
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const result = await runtime.upsertScheduledTask('project-test', task({ useDefaults: true }));
|
|
79
|
+
expect(result.task.execution).toEqual({ prompt: 'Summarize', useDefaults: true });
|
|
80
|
+
await expect(runtime.upsertScheduledTask('project-test', task({}))).rejects.toThrow('execution.providerID is required');
|
|
81
|
+
} finally {
|
|
82
|
+
await cleanup();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
56
86
|
it('rejects invalid cron expressions', async () => {
|
|
57
87
|
const { runtime, cleanup } = await createRuntime();
|
|
58
88
|
try {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// this file) use the same code paths so a value reads back the same on every
|
|
13
13
|
// surface.
|
|
14
14
|
|
|
15
|
+
import { isRecord as isObjectRecord } from '../shared/guards.js';
|
|
15
16
|
import crypto from 'node:crypto';
|
|
16
17
|
|
|
17
18
|
const ACTION_NAME_MAX_LENGTH = 80;
|
|
@@ -24,8 +25,6 @@ const SETUP_COMMANDS_MAX = 50;
|
|
|
24
25
|
const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
|
|
25
26
|
const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
|
|
26
27
|
|
|
27
|
-
const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
-
|
|
29
28
|
const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
|
|
30
29
|
|
|
31
30
|
const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* `req.body` is parsed here without a per-route parser.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { isRecord as isObjectRecord } from '../shared/guards.js';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import { z } from 'zod';
|
|
16
17
|
import { createProjectIdFromPath } from './project-id.js';
|
|
17
18
|
import { isProjectSetupValidationError } from './project-setup.js';
|
|
18
19
|
|
|
19
|
-
const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
-
|
|
21
20
|
const directoryRequestSchema = z.object({
|
|
22
21
|
path: z.string().trim().catch(''),
|
|
23
22
|
create: z.literal(true).catch(false),
|
|
@@ -51,7 +51,7 @@ asked, `/api/quota/providers` answers 500 instead of an empty list.
|
|
|
51
51
|
| `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` / `providers/minimax-shared.js` | `minimax-cn-coding-plan` |
|
|
52
52
|
| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Manual cookie pasted into Settings (`aid=...; __Secure-session=...` from `ollama.com`), stored under `~/.config/openchamber/quota/` |
|
|
53
53
|
| `wafer` | Wafer.ai | `providers/wafer.js` | `wafer`, `wafer-ai`, `wafer_ai`, `wafer.ai` |
|
|
54
|
-
| `opencode-go` | OpenCode Go | `providers/opencode-go.js` | `opencode-go` API key from OpenCode `auth.json` |
|
|
54
|
+
| `opencode-go` | OpenCode Go | `providers/opencode-go.js` | `opencode-go` API key from OpenCode `auth.json`, or an OpenCode Console OAuth sign-in on the shared `opencode` integration |
|
|
55
55
|
| `neuralwatt` | NeuralWatt | `providers/neuralwatt.js` | `neuralwatt` (API key under `key` or `token`) |
|
|
56
56
|
| `kilo` | Kilo Code | `providers/kilo.js` | `kilo`, `kilocode`, `kilo-code` (API key under `key`, `token`, or OAuth `access`; organization id under `kilocodeOrganizationId`, `organizationId`, or OAuth `accountId`, else OpenCode `provider.kilo.options.kilocodeOrganizationId`) |
|
|
57
57
|
| `zenmux` | ZenMux | `providers/zenmux.js` | Optional Platform API key stored under `~/.config/openchamber/quota/zenmux.json` (`platformApiKey`). The OpenCode chat key is not used. |
|
|
@@ -69,10 +69,21 @@ All providers should return results via shared helpers to preserve API shape:
|
|
|
69
69
|
Provider modules must export `providerId`, `providerName`, `aliases`, `isConfigured(auth?)`, and `fetchQuota()`.
|
|
70
70
|
`fetchQuota()` should return a quota result with `usage.windows` keyed by window name (for example `5h`, `7d`, `daily`) and optional provider-specific `usage.models` data.
|
|
71
71
|
|
|
72
|
-
exe.dev, Ollama Cloud, Cursor, and ZenMux credentials are explicitly managed through Settings. exe.dev usage uses a separately generated HTTPS API token restricted to `billing credits usage` and aggregates every `exe-*` model provider into one monthly credit window. Generate the token with `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`. ZenMux usage uses an optional Platform API key pasted on the Usage page; the OpenCode chat key is never used, and a missing Platform API key means ZenMux quota is not fetched.
|
|
72
|
+
exe.dev, Ollama Cloud, Cursor, and ZenMux credentials are explicitly managed through Settings. exe.dev usage uses a separately generated HTTPS API token restricted to `billing credits usage` and aggregates every `exe-*` model provider into one monthly credit window. Generate the token with `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`. ZenMux usage uses an optional Platform API key pasted on the Usage page; the OpenCode chat key is never used, and a missing Platform API key means ZenMux quota is not fetched. The server validates managed credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
|
|
73
73
|
|
|
74
74
|
Command Code usage resolves account scope through `GET /alpha/whoami`, then reads server-backed credit balances and five-hour/weekly limits from `GET /alpha/billing/credits?orgId=...`. Personal accounts return `org: null` and use `/alpha/billing/credits` without an `orgId`; organization accounts include their organization id. Web/Electron and VS Code read the standard `command-code` OpenCode auth entry (including OAuth `access`) or `COMMAND_CODE_API_KEY`; credentials remain in the owning runtime and are never returned to shared UI.
|
|
75
75
|
|
|
76
|
+
## OpenCode Go credential and quota semantics
|
|
77
|
+
|
|
78
|
+
OpenCode Go has two credential shapes and they are not interchangeable. A service-account key stored under the `opencode-go` integration is a bearer token for `GET https://opencode.ai/zen/go/v1/usage` with the stable `x-opencode-session: openchamber-usage` workload id. A Console sign-in lives on the shared `opencode` integration as OAuth; its access token is a user session, not that key, and it is sent to `GET https://opencode.ai/console/api/go/status` with `x-org-id: <selected organization ID>`. That status endpoint is what the current Console Go page itself reads; it is not publicly documented, so re-check it against the live Console page before assuming it is still current.
|
|
79
|
+
|
|
80
|
+
- **Credential selection**: OpenCode marks exactly one credential active per integration and credentials are global, so the active `opencode` OAuth credential is the selected account and organization. The reader carries only the Console `server` and `orgID` metadata; a credential whose `server` is not `https://opencode.ai/console`, whose `orgID` is missing or malformed, or that is an API key does not count as a Console sign-in. When both exist, the Console sign-in serves Go and the `opencode-go` key is the fallback, both when there is no Console sign-in and when the Console read fails; the Console error surfaces only when there is no key.
|
|
81
|
+
- **Read-only and per-call**: the access token is read through `../opencode/auth.js` on every fetch, so switching accounts drops the previous account instead of showing stale numbers. OpenChamber never refreshes the token itself: OpenCode owns that, and an expired access token (local `expires`, or a 401/403) is surfaced as "sign in again" rather than a competing refresh.
|
|
82
|
+
- **Meters**: the response's `product` must be `go` or `go-plus`; anything else is an absent subscription. `access.meters.fiveHour` / `week` / `month` map to the existing `5h` / `weekly` / `monthly` windows. `limitMicroCents` and `usedMicroCents` are decimal strings; the percentage is `used / limit`, never a hardcoded plan limit. A missing meter, a non-positive or unparseable limit, or an invalid reset timestamp skips that window alone; no usable window is a failed refresh, not zero usage.
|
|
83
|
+
- **Trusted origin**: requests go only to the fixed Console status URL with `redirect: 'error'`, so the bearer token is never forwarded to another origin.
|
|
84
|
+
- **Failure classes stay distinct**: not configured (no credential), expired sign-in (expired local token or 401/403), absent subscription (unexpected `product`), malformed response, and temporary request failure each return their own configured flag and message.
|
|
85
|
+
- Keep `packages/web/server/lib/quota/providers/opencode-go.js` and `packages/vscode/src/opencodeGoQuota.ts` in sync — the VS Code extension duplicates this logic rather than importing the web provider.
|
|
86
|
+
|
|
76
87
|
On the first OpenCode Go usage refresh after upgrading, OpenChamber deletes the obsolete `quota/opencode-go.json` credential file without reading its cookie value.
|
|
77
88
|
|
|
78
89
|
## Codex credit balance semantics
|
|
@@ -149,13 +160,30 @@ Web and VS Code accept finite numeric balances and non-empty numeric strings. Mi
|
|
|
149
160
|
|
|
150
161
|
## Kimi for Coding field semantics
|
|
151
162
|
|
|
152
|
-
`GET https://api.kimi.com/coding/v1/usages` is
|
|
153
|
-
|
|
154
|
-
-
|
|
163
|
+
`GET https://api.kimi.com/coding/v1/usages` has two payload shapes and the provider supports both. Which one arrives depends on whether the account's plan has been migrated; in 2026-10 Kimi moved its newer plans from a weekly allowance to a monthly one. The endpoint is not in Kimi's public Open Platform reference. Its contract is defined by Kimi's own CLI, `MoonshotAI/kimi-code`, in `packages/oauth/src/managed-usage.ts` and `apps/kimi-code/src/utils/usage/usage-format.ts`.
|
|
164
|
+
|
|
165
|
+
**Ratio payload.** A top-level `usages` object keyed by limit name, each entry `{ used_ratio, reset_time }`, where `used_ratio` is a 0-1 fraction and `reset_time` is RFC3339. Kimi documents its client as rendering whichever entries are served, so an absent key means the plan has no such window rather than missing data. An entry with no finite `used_ratio` is skipped instead of shown as an unknown percentage.
|
|
166
|
+
|
|
167
|
+
| `usages` key | Window | Notes |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `limit_5h` | `5h` | Rolling five-hour frequency window |
|
|
170
|
+
| `limit_7d` | `weekly` | Served only by plans that still have a weekly allowance |
|
|
171
|
+
| `limit_month_total` | `monthly` | The plan's monthly allowance; `reset_time` is the subscription anchor, not a calendar month boundary |
|
|
172
|
+
| `limit_month_code` | not shown | The code-typed share of `limit_month_total`, not a second allowance |
|
|
173
|
+
|
|
174
|
+
`limit_month_code` is not surfaced. As its own row it would read as a second monthly allowance. As a `valueLabel` on the monthly row it would replace the row's percent with fixed used figures, so the row would ignore the used/remaining display setting that every other row follows.
|
|
175
|
+
|
|
176
|
+
When `usages` is present the provider does not read `limits[]`. Its 300-minute entry describes the same five-hour window as `limit_5h`, carrying the same ratio rounded to a whole percentage against a fixed `limit` of `100`, and Kimi's own client ignores `limits[]` altogether.
|
|
177
|
+
|
|
178
|
+
The payload may also carry `boosterWallet`, the pay-as-you-go top-up wallet that Kimi's client reads as `extraUsage`. OpenChamber does not surface it.
|
|
179
|
+
|
|
180
|
+
**Counted payload.** Plans that have not migrated return a single weekly `usage` block plus `limits[]` rate-limit entries. This shape is inconsistent about which field carries consumption: the `usage` block returns `used` (consumed) with no `remaining` field, while each `limits[].detail` block returns `remaining` (available) with no `used` field. The provider computes `usedPercent` from whichever of the two is present, `used` taking precedence when both exist, rather than assuming one field name. A `limits[]` entry whose window is exactly five hours is labeled `Rate Limit (<raw duration>)` to set it apart from a plain duration.
|
|
181
|
+
|
|
182
|
+
Credentials resolve in alias order, first match wins. OpenCode's China plan id `kimi-code-plan-cn` (kimi.com) comes before the pre-split `kimi-for-coding` and `kimi` ids, because a leftover pre-split key can hold a dead credential that would otherwise shadow the live China plan key and return 401. The global plan id `kimi-code-plan-global` (kimi.ai) stays last. Kimi's client uses `https://api.kimi.ai/coding/v1/usages` for global accounts, while this provider calls `api.kimi.com` for every alias, so a global-only key gets a 401 here. That gap is known and unfixed.
|
|
155
183
|
|
|
156
|
-
|
|
184
|
+
Many Kimi users hold a pay-as-you-go Moonshot platform key (prepaid vouchers, no Kimi Code subscription) under `KIMI_API_KEY`. The Kimi Code usage address refuses such a key with 401. On a 401 or 403 the provider therefore asks `GET https://api.moonshot.ai/v1/users/me/balance` with the same key and, when that answers with a numeric `data.available_balance`, reports it as a `credits_balance` window (USD, remaining only). If the balance read also fails, the original `API error: <status>` is returned; other HTTP errors never try the balance address.
|
|
157
185
|
|
|
158
|
-
|
|
186
|
+
Both `packages/web/server/lib/quota/providers/kimi.js` and `packages/vscode/src/quotaProviders.ts` (`fetchKimiQuota`) must stay in sync — the VS Code extension duplicates this parsing logic rather than importing it.
|
|
159
187
|
|
|
160
188
|
## Ollama Cloud settings-page shapes
|
|
161
189
|
|
|
@@ -16,7 +16,7 @@ import { toClaudeUsage } from './transforms.js';
|
|
|
16
16
|
|
|
17
17
|
export const providerId = 'claude';
|
|
18
18
|
export const providerName = 'Claude';
|
|
19
|
-
|
|
19
|
+
const aliases = ['anthropic', 'claude'];
|
|
20
20
|
|
|
21
21
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
22
22
|
const OAUTH_BETA_HEADER = 'oauth-2025-04-20';
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export const providerId = 'cline-pass';
|
|
14
14
|
export const providerName = 'ClinePass';
|
|
15
|
-
|
|
15
|
+
const aliases = ['cline-pass'];
|
|
16
16
|
const CLINE_USAGE_URL = 'https://api.cline.bot/api/v1/users/me/plan/usage-limits';
|
|
17
17
|
|
|
18
18
|
// Cline reports a rolling five-hour window, a rolling weekly window, and a
|
|
@@ -3,7 +3,7 @@ import { asObject, buildResult, formatMoney, toNumber, toTimestamp, toUsageWindo
|
|
|
3
3
|
|
|
4
4
|
export const providerId = 'exe-dev';
|
|
5
5
|
export const providerName = 'exe.dev';
|
|
6
|
-
|
|
6
|
+
const aliases = ['exe-dev'];
|
|
7
7
|
const EXEC_URL = 'https://exe.dev/exec';
|
|
8
8
|
const USAGE_COMMAND = 'billing credits usage --group=day --json';
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ export { resolveGoogleAuthSources } from './auth.js';
|
|
|
16
16
|
|
|
17
17
|
export const providerId = 'google';
|
|
18
18
|
export const providerName = 'Google';
|
|
19
|
-
|
|
19
|
+
const aliases = ['google', 'google.oauth'];
|
|
20
20
|
|
|
21
21
|
export const isConfigured = (auth) => resolveGoogleAuthSources(auth).length > 0;
|
|
22
22
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export const providerId = 'hyper';
|
|
14
14
|
export const providerName = 'Charm Hyper';
|
|
15
|
-
|
|
15
|
+
const aliases = ['hyper'];
|
|
16
16
|
const HYPER_QUOTA_URL = 'https://hyper.charm.land/v1/credits';
|
|
17
17
|
const CREDIT_TO_USD = 0.05;
|
|
18
18
|
|
|
@@ -8,9 +8,40 @@ import {
|
|
|
8
8
|
toTimestamp,
|
|
9
9
|
durationToLabel,
|
|
10
10
|
durationToSeconds,
|
|
11
|
-
asNonEmptyString
|
|
11
|
+
asNonEmptyString,
|
|
12
|
+
formatMoney,
|
|
13
|
+
asObject
|
|
12
14
|
} from '../utils/index.js';
|
|
13
15
|
|
|
16
|
+
const MOONSHOT_BALANCE_URL = 'https://api.moonshot.ai/v1/users/me/balance';
|
|
17
|
+
|
|
18
|
+
// A pay-as-you-go Moonshot platform key (prepaid vouchers, no Kimi Code
|
|
19
|
+
// subscription) is refused by the Kimi Code usage address. Its balance is read
|
|
20
|
+
// from the platform instead: USD, what remains, with no spent figure.
|
|
21
|
+
// Resolves to the credits_balance windows, or null when they cannot be read.
|
|
22
|
+
const fetchMoonshotBalanceWindows = async (apiKey, fetchImpl) => {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchImpl(MOONSHOT_BALANCE_URL, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
27
|
+
signal: AbortSignal.timeout(15_000)
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) return null;
|
|
30
|
+
const balance = asObject(asObject(await response.json())?.data)?.available_balance;
|
|
31
|
+
if (!Number.isFinite(balance)) return null;
|
|
32
|
+
return {
|
|
33
|
+
credits_balance: toUsageWindow({
|
|
34
|
+
usedPercent: null,
|
|
35
|
+
windowSeconds: null,
|
|
36
|
+
resetAt: null,
|
|
37
|
+
valueLabel: `$${formatMoney(balance)}`
|
|
38
|
+
})
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
14
45
|
export const providerId = 'kimi-for-coding';
|
|
15
46
|
export const providerName = 'Kimi for Coding';
|
|
16
47
|
// OpenCode stores the Kimi For Coding plans as `kimi-code-plan-cn` (kimi.com)
|
|
@@ -18,11 +49,47 @@ export const providerName = 'Kimi for Coding';
|
|
|
18
49
|
// works at the api.kimi.com usage address, and a pre-split `kimi-for-coding`
|
|
19
50
|
// key left behind with a dead credential must not shadow it. The global plan
|
|
20
51
|
// stays last, as before, since its key is not known to work at that address.
|
|
21
|
-
|
|
52
|
+
const aliases = ['kimi-code-plan-cn', 'kimi-for-coding', 'kimi', 'kimi-code-plan-global'];
|
|
53
|
+
|
|
54
|
+
// The ratio-based `usages` block, in the order Kimi's own CLI renders it.
|
|
55
|
+
// `limit_month_code` is not read: it is the code-typed share of
|
|
56
|
+
// `limit_month_total`, not a second allowance, and a value label carrying it
|
|
57
|
+
// would replace the monthly percent and stay a used figure when the panel shows
|
|
58
|
+
// what remains. `limit_7d` arrives only on plans that still have a weekly
|
|
59
|
+
// quota, and Kimi documents the client as rendering whichever entries are served.
|
|
60
|
+
const RATIO_WINDOWS = [
|
|
61
|
+
{ key: 'limit_5h', label: '5h', windowSeconds: 5 * 60 * 60 },
|
|
62
|
+
{ key: 'limit_7d', label: 'weekly', windowSeconds: 7 * 24 * 60 * 60 },
|
|
63
|
+
{ key: 'limit_month_total', label: 'monthly', windowSeconds: null }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// `used_ratio` is a 0-1 fraction. An entry without a finite ratio carries no
|
|
67
|
+
// information, so it is skipped rather than shown as an unknown percentage.
|
|
68
|
+
const toRatioPercent = (entry) => {
|
|
69
|
+
const ratio = toNumber(entry?.used_ratio);
|
|
70
|
+
return ratio === null ? null : Math.max(0, Math.min(100, ratio * 100));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ratioWindows = (usages) => {
|
|
74
|
+
const windows = {};
|
|
75
|
+
for (const { key, label, windowSeconds } of RATIO_WINDOWS) {
|
|
76
|
+
const entry = asObject(usages[key]);
|
|
77
|
+
const usedPercent = toRatioPercent(entry);
|
|
78
|
+
if (usedPercent === null) continue;
|
|
79
|
+
windows[label] = toUsageWindow({
|
|
80
|
+
usedPercent,
|
|
81
|
+
windowSeconds,
|
|
82
|
+
resetAt: toTimestamp(entry.reset_time)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return windows;
|
|
86
|
+
};
|
|
22
87
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
88
|
+
// The pre-ratio payload: one counted weekly `usage` block plus `limits[]`
|
|
89
|
+
// rate-limit entries. Plans that have not migrated still answer this way. Its
|
|
90
|
+
// `usage` block reports `used`; the `limits[].detail` blocks report `remaining`
|
|
91
|
+
// instead. Neither field is guaranteed present, so derive usedPercent from
|
|
92
|
+
// whichever one the API actually returned.
|
|
26
93
|
const computeUsedPercent = (total, used, remaining) => {
|
|
27
94
|
if (!total) return null;
|
|
28
95
|
if (used !== null) {
|
|
@@ -34,6 +101,33 @@ const computeUsedPercent = (total, used, remaining) => {
|
|
|
34
101
|
return null;
|
|
35
102
|
};
|
|
36
103
|
|
|
104
|
+
const legacyWindows = (payload) => {
|
|
105
|
+
const windows = {};
|
|
106
|
+
const usage = asObject(payload?.usage);
|
|
107
|
+
if (usage) {
|
|
108
|
+
windows.weekly = toUsageWindow({
|
|
109
|
+
usedPercent: computeUsedPercent(toNumber(usage.limit), toNumber(usage.used), toNumber(usage.remaining)),
|
|
110
|
+
windowSeconds: null,
|
|
111
|
+
resetAt: toTimestamp(usage.resetTime)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const limits = Array.isArray(payload?.limits) ? payload.limits : [];
|
|
116
|
+
for (const limit of limits) {
|
|
117
|
+
const window = limit?.window;
|
|
118
|
+
const detail = limit?.detail;
|
|
119
|
+
const rawLabel = durationToLabel(window?.duration, window?.timeUnit);
|
|
120
|
+
const windowSeconds = durationToSeconds(window?.duration, window?.timeUnit);
|
|
121
|
+
const label = windowSeconds === 5 * 60 * 60 ? `Rate Limit (${rawLabel})` : rawLabel;
|
|
122
|
+
windows[label] = toUsageWindow({
|
|
123
|
+
usedPercent: computeUsedPercent(toNumber(detail?.limit), toNumber(detail?.used), toNumber(detail?.remaining)),
|
|
124
|
+
windowSeconds,
|
|
125
|
+
resetAt: toTimestamp(detail?.resetTime)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return windows;
|
|
129
|
+
};
|
|
130
|
+
|
|
37
131
|
const getApiKey = (auth) => {
|
|
38
132
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
39
133
|
return asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
|
|
@@ -64,6 +158,18 @@ export const fetchQuota = async ({ readAuth = readOpenCodeCredentials, fetchImpl
|
|
|
64
158
|
});
|
|
65
159
|
|
|
66
160
|
if (!response.ok) {
|
|
161
|
+
if (response.status === 401 || response.status === 403) {
|
|
162
|
+
const balanceWindows = await fetchMoonshotBalanceWindows(apiKey, fetchImpl);
|
|
163
|
+
if (balanceWindows) {
|
|
164
|
+
return buildResult({
|
|
165
|
+
providerId,
|
|
166
|
+
providerName,
|
|
167
|
+
ok: true,
|
|
168
|
+
configured: true,
|
|
169
|
+
usage: { windows: balanceWindows }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
67
173
|
return buildResult({
|
|
68
174
|
providerId,
|
|
69
175
|
providerName,
|
|
@@ -74,37 +180,11 @@ export const fetchQuota = async ({ readAuth = readOpenCodeCredentials, fetchImpl
|
|
|
74
180
|
}
|
|
75
181
|
|
|
76
182
|
const payload = await response.json();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const remaining = toNumber(usage.remaining);
|
|
83
|
-
const usedPercent = computeUsedPercent(limit, used, remaining);
|
|
84
|
-
windows.weekly = toUsageWindow({
|
|
85
|
-
usedPercent,
|
|
86
|
-
windowSeconds: null,
|
|
87
|
-
resetAt: toTimestamp(usage.resetTime)
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const limits = Array.isArray(payload?.limits) ? payload.limits : [];
|
|
92
|
-
for (const limit of limits) {
|
|
93
|
-
const window = limit?.window;
|
|
94
|
-
const detail = limit?.detail;
|
|
95
|
-
const rawLabel = durationToLabel(window?.duration, window?.timeUnit);
|
|
96
|
-
const windowSeconds = durationToSeconds(window?.duration, window?.timeUnit);
|
|
97
|
-
const label = windowSeconds === 5 * 60 * 60 ? `Rate Limit (${rawLabel})` : rawLabel;
|
|
98
|
-
const total = toNumber(detail?.limit);
|
|
99
|
-
const used = toNumber(detail?.used);
|
|
100
|
-
const remaining = toNumber(detail?.remaining);
|
|
101
|
-
const usedPercent = computeUsedPercent(total, used, remaining);
|
|
102
|
-
windows[label] = toUsageWindow({
|
|
103
|
-
usedPercent,
|
|
104
|
-
windowSeconds,
|
|
105
|
-
resetAt: toTimestamp(detail?.resetTime)
|
|
106
|
-
});
|
|
107
|
-
}
|
|
183
|
+
// Kimi replaced the counted payload with a ratio-based `usages` map. Its own
|
|
184
|
+
// client reads only `usages`, so when that block is present the `limits[]`
|
|
185
|
+
// entry for the same 5-hour window is a coarser duplicate and is not read.
|
|
186
|
+
const usages = asObject(asObject(payload)?.usages);
|
|
187
|
+
const windows = usages ? ratioWindows(usages) : legacyWindows(payload);
|
|
108
188
|
|
|
109
189
|
return buildResult({
|
|
110
190
|
providerId,
|