@aiworker/web 1.22.0 → 1.23.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 +4 -4
- package/bin/cli.js +14 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +19 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +19 -17
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +15 -18
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +91 -30
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/DiffView-DluiRSla.js +0 -4
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../opencode/auth.js', () => ({
|
|
4
|
+
readAuthFile: () => ({ openrouter: { key: 'test-token' } }),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { fetchQuota, resolveResetAt } from './openrouter.js';
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllGlobals();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockResponse = (body, init = {}) => ({
|
|
14
|
+
ok: true,
|
|
15
|
+
status: 200,
|
|
16
|
+
json: async () => body,
|
|
17
|
+
...init,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Documented payload shape from https://openrouter.ai/docs/api_reference/limits
|
|
21
|
+
const DOCUMENTED_PAYLOAD = {
|
|
22
|
+
data: {
|
|
23
|
+
label: 'Default',
|
|
24
|
+
limit: 30,
|
|
25
|
+
limit_remaining: 25,
|
|
26
|
+
limit_reset: 'monthly',
|
|
27
|
+
include_byok_in_limit: false,
|
|
28
|
+
usage: 5,
|
|
29
|
+
usage_daily: 1,
|
|
30
|
+
usage_weekly: 3,
|
|
31
|
+
usage_monthly: 5,
|
|
32
|
+
byok_usage: 0,
|
|
33
|
+
byok_usage_daily: 0,
|
|
34
|
+
byok_usage_weekly: 0,
|
|
35
|
+
byok_usage_monthly: 0,
|
|
36
|
+
is_free_tier: false,
|
|
37
|
+
is_management_key: false,
|
|
38
|
+
is_provisioning_key: false,
|
|
39
|
+
creator_user_id: 'user-fixture',
|
|
40
|
+
expires_at: null,
|
|
41
|
+
rate_limit: null
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const expectMonthlyReset = (resetAt) => {
|
|
46
|
+
expect(resetAt).toEqual(expect.any(Number));
|
|
47
|
+
const resetDate = new Date(resetAt);
|
|
48
|
+
expect(resetDate.getUTCDate()).toBe(1);
|
|
49
|
+
expect(resetDate.getUTCHours()).toBe(0);
|
|
50
|
+
expect(resetAt - Date.now()).toBeGreaterThan(0);
|
|
51
|
+
expect(resetAt - Date.now()).toBeLessThanOrEqual(31 * 86400 * 1000);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('OpenRouter quota provider', () => {
|
|
55
|
+
it('builds a monthly window from the documented payload', async () => {
|
|
56
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(DOCUMENTED_PAYLOAD)));
|
|
57
|
+
|
|
58
|
+
const result = await fetchQuota();
|
|
59
|
+
const window = result.usage.windows.monthly;
|
|
60
|
+
|
|
61
|
+
expect(result.ok).toBe(true);
|
|
62
|
+
expect(result.providerId).toBe('openrouter');
|
|
63
|
+
expect(window.usedPercent).toBeCloseTo(16.6667, 3);
|
|
64
|
+
expect(window.valueLabel).toBe('$5.00 / $30.00');
|
|
65
|
+
expect(window.windowSeconds).toBe(30 * 86400);
|
|
66
|
+
expectMonthlyReset(window.resetAt);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses the daily window and preserves the observed funded-key values', async () => {
|
|
70
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
71
|
+
data: {
|
|
72
|
+
usage: 3.17561396,
|
|
73
|
+
usage_daily: 0.0000018,
|
|
74
|
+
usage_weekly: 0.0000018,
|
|
75
|
+
usage_monthly: 3.17561396,
|
|
76
|
+
limit: 30,
|
|
77
|
+
limit_remaining: 29.9999982,
|
|
78
|
+
limit_reset: 'daily',
|
|
79
|
+
is_free_tier: true,
|
|
80
|
+
is_management_key: false,
|
|
81
|
+
include_byok_in_limit: false,
|
|
82
|
+
byok_usage: 0
|
|
83
|
+
}
|
|
84
|
+
})));
|
|
85
|
+
|
|
86
|
+
const result = await fetchQuota();
|
|
87
|
+
const window = result.usage.windows.daily;
|
|
88
|
+
|
|
89
|
+
expect(window).toBeDefined();
|
|
90
|
+
expect(window.windowSeconds).toBe(86400);
|
|
91
|
+
expect(window.valueLabel).toBe('$0.00 / $30.00');
|
|
92
|
+
expect(window.usedPercent).toBeLessThan(0.001);
|
|
93
|
+
expect(window.resetAt % 86400000).toBe(0);
|
|
94
|
+
expect(window.resetAt - Date.now()).toBeGreaterThan(0);
|
|
95
|
+
expect(window.resetAt - Date.now()).toBeLessThanOrEqual(86400000);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('builds a monthly unlimited window from null limit fields', async () => {
|
|
99
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
100
|
+
data: {
|
|
101
|
+
limit: null,
|
|
102
|
+
limit_remaining: null,
|
|
103
|
+
limit_reset: null,
|
|
104
|
+
usage_monthly: 3.17561396,
|
|
105
|
+
is_management_key: false
|
|
106
|
+
}
|
|
107
|
+
})));
|
|
108
|
+
|
|
109
|
+
const result = await fetchQuota();
|
|
110
|
+
const window = result.usage.windows.monthly;
|
|
111
|
+
|
|
112
|
+
expect(window.usedPercent).toBeNull();
|
|
113
|
+
expect(window.valueLabel).toBe('$3.18 spent');
|
|
114
|
+
expect(window.windowSeconds).toBe(30 * 86400);
|
|
115
|
+
expectMonthlyReset(window.resetAt);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('uses a credits window for a lifetime cap', async () => {
|
|
119
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
120
|
+
data: { limit: 30, limit_remaining: 25, limit_reset: null, usage_monthly: 5 }
|
|
121
|
+
})));
|
|
122
|
+
|
|
123
|
+
const result = await fetchQuota();
|
|
124
|
+
const window = result.usage.windows.credits;
|
|
125
|
+
|
|
126
|
+
expect(window).toBeDefined();
|
|
127
|
+
expect(window.resetAt).toBeNull();
|
|
128
|
+
expect(window.windowSeconds).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses a credits window for an unrecognized reset period', async () => {
|
|
132
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
133
|
+
data: { limit: 30, limit_remaining: 25, limit_reset: 'yearly', usage_monthly: 5 }
|
|
134
|
+
})));
|
|
135
|
+
|
|
136
|
+
const result = await fetchQuota();
|
|
137
|
+
const window = result.usage.windows.credits;
|
|
138
|
+
|
|
139
|
+
expect(window).toBeDefined();
|
|
140
|
+
expect(window.resetAt).toBeNull();
|
|
141
|
+
expect(window.windowSeconds).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('builds a weekly window from a weekly limit', async () => {
|
|
145
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
146
|
+
data: { limit: 30, limit_remaining: 25, limit_reset: 'weekly', usage_monthly: 5 }
|
|
147
|
+
})));
|
|
148
|
+
|
|
149
|
+
const result = await fetchQuota();
|
|
150
|
+
const window = result.usage.windows.weekly;
|
|
151
|
+
|
|
152
|
+
expect(window).toBeDefined();
|
|
153
|
+
expect(window.windowSeconds).toBe(604800);
|
|
154
|
+
expect(new Date(window.resetAt).getUTCDay()).toBe(1);
|
|
155
|
+
expect(window.resetAt - Date.now()).toBeGreaterThan(0);
|
|
156
|
+
expect(window.resetAt - Date.now()).toBeLessThanOrEqual(7 * 86400 * 1000);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('ignores BYOK usage when calculating the quota label and percent', async () => {
|
|
160
|
+
const withByokPayload = {
|
|
161
|
+
...DOCUMENTED_PAYLOAD,
|
|
162
|
+
data: {
|
|
163
|
+
...DOCUMENTED_PAYLOAD.data,
|
|
164
|
+
include_byok_in_limit: true,
|
|
165
|
+
byok_usage_monthly: 100,
|
|
166
|
+
byok_usage: 100
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const fetchMock = vi.fn()
|
|
170
|
+
.mockResolvedValueOnce(mockResponse(DOCUMENTED_PAYLOAD))
|
|
171
|
+
.mockResolvedValueOnce(mockResponse(withByokPayload));
|
|
172
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
173
|
+
|
|
174
|
+
const withoutByok = await fetchQuota();
|
|
175
|
+
const withByok = await fetchQuota();
|
|
176
|
+
|
|
177
|
+
expect(withByok.usage.windows.monthly.valueLabel)
|
|
178
|
+
.toBe(withoutByok.usage.windows.monthly.valueLabel);
|
|
179
|
+
expect(withByok.usage.windows.monthly.usedPercent)
|
|
180
|
+
.toBe(withoutByok.usage.windows.monthly.usedPercent);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('rejects management keys with a specific error', async () => {
|
|
184
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
185
|
+
data: { is_management_key: true }
|
|
186
|
+
})));
|
|
187
|
+
|
|
188
|
+
const result = await fetchQuota();
|
|
189
|
+
|
|
190
|
+
expect(result.ok).toBe(false);
|
|
191
|
+
expect(result.configured).toBe(true);
|
|
192
|
+
expect(result.usage).toBeNull();
|
|
193
|
+
expect(result.error).toBe('Management key configured — quota needs an inference API key');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it.each([401, 403])('maps %s to a session-expired error', async (status) => {
|
|
197
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status }));
|
|
198
|
+
|
|
199
|
+
const result = await fetchQuota();
|
|
200
|
+
|
|
201
|
+
expect(result.ok).toBe(false);
|
|
202
|
+
expect(result.error).toBe('Session expired — please re-authenticate with OpenRouter');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('reports invalid-response on JSON parse failure', async () => {
|
|
206
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
207
|
+
ok: true,
|
|
208
|
+
status: 200,
|
|
209
|
+
json: async () => { throw new SyntaxError('Unexpected token'); },
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const result = await fetchQuota();
|
|
213
|
+
|
|
214
|
+
expect(result.ok).toBe(false);
|
|
215
|
+
expect(result.error).toBe('Invalid response from provider');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('reports a normalized timeout error', async () => {
|
|
219
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new DOMException('The operation timed out.', 'TimeoutError')));
|
|
220
|
+
|
|
221
|
+
const result = await fetchQuota();
|
|
222
|
+
|
|
223
|
+
expect(result.ok).toBe(false);
|
|
224
|
+
expect(result.error).toBe('Request timed out');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns no-quota-data when data is missing', async () => {
|
|
228
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({})));
|
|
229
|
+
|
|
230
|
+
const result = await fetchQuota();
|
|
231
|
+
|
|
232
|
+
expect(result.ok).toBe(false);
|
|
233
|
+
expect(result.configured).toBe(true);
|
|
234
|
+
expect(result.error).toBe('No quota data in response');
|
|
235
|
+
expect(result.usage).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('returns no-quota-data when data is empty', async () => {
|
|
239
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({ data: {} })));
|
|
240
|
+
|
|
241
|
+
const result = await fetchQuota();
|
|
242
|
+
|
|
243
|
+
expect(result.ok).toBe(false);
|
|
244
|
+
expect(result.configured).toBe(true);
|
|
245
|
+
expect(result.error).toBe('No quota data in response');
|
|
246
|
+
expect(result.usage).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it.each([null, []])('returns no-quota-data when data is %s', async (data) => {
|
|
250
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({ data })));
|
|
251
|
+
|
|
252
|
+
const result = await fetchQuota();
|
|
253
|
+
|
|
254
|
+
expect(result.ok).toBe(false);
|
|
255
|
+
expect(result.configured).toBe(true);
|
|
256
|
+
expect(result.usage).toBeNull();
|
|
257
|
+
expect(result.error).toBe('No quota data in response');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns no-quota-data when a limit has no remaining value', async () => {
|
|
261
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
262
|
+
data: { limit: 30, limit_remaining: null, usage_monthly: 5 }
|
|
263
|
+
})));
|
|
264
|
+
|
|
265
|
+
const result = await fetchQuota();
|
|
266
|
+
|
|
267
|
+
expect(result.ok).toBe(false);
|
|
268
|
+
expect(result.error).toBe('No quota data in response');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('keeps a zero limit valid with a null percent', async () => {
|
|
272
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
273
|
+
data: { limit: 0, limit_remaining: 0, limit_reset: 'monthly', usage_monthly: 0 }
|
|
274
|
+
})));
|
|
275
|
+
|
|
276
|
+
const result = await fetchQuota();
|
|
277
|
+
const window = result.usage.windows.monthly;
|
|
278
|
+
|
|
279
|
+
expect(result.ok).toBe(true);
|
|
280
|
+
expect(window.usedPercent).toBeNull();
|
|
281
|
+
expect(window.valueLabel).toBe('$0.00 / $0.00');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('requests the key endpoint and never the credits endpoint', async () => {
|
|
285
|
+
const fetchMock = vi.fn().mockResolvedValue(mockResponse(DOCUMENTED_PAYLOAD));
|
|
286
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
287
|
+
|
|
288
|
+
await fetchQuota();
|
|
289
|
+
|
|
290
|
+
const requestedUrl = fetchMock.mock.calls[0][0];
|
|
291
|
+
expect(requestedUrl).toBe('https://openrouter.ai/api/v1/key');
|
|
292
|
+
expect(requestedUrl).not.toContain('/api/v1/credits');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('resolves daily reset at the next UTC day across year boundaries', () => {
|
|
296
|
+
expect(resolveResetAt('daily', Date.UTC(2024, 11, 31, 23, 59))).toBe(Date.UTC(2025, 0, 1));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('resolves weekly reset from Sunday to the next Monday', () => {
|
|
300
|
+
expect(resolveResetAt('weekly', Date.UTC(2024, 0, 7, 12))).toBe(Date.UTC(2024, 0, 8));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('resolves weekly reset from Monday to the following Monday', () => {
|
|
304
|
+
expect(resolveResetAt('weekly', Date.UTC(2024, 0, 8, 12))).toBe(Date.UTC(2024, 0, 15));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('resolves weekly reset from Wednesday to the next Monday', () => {
|
|
308
|
+
expect(resolveResetAt('weekly', Date.UTC(2024, 0, 10, 12))).toBe(Date.UTC(2024, 0, 15));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('resolves monthly reset from January 31 to February 1', () => {
|
|
312
|
+
expect(resolveResetAt('monthly', Date.UTC(2024, 0, 31, 12))).toBe(Date.UTC(2024, 1, 1));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('resolves monthly reset across December to January', () => {
|
|
316
|
+
expect(resolveResetAt('monthly', Date.UTC(2024, 11, 31, 23, 59))).toBe(Date.UTC(2025, 0, 1));
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('clamps percent at 100 while leaving the money label unclamped', async () => {
|
|
320
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
321
|
+
data: { limit: 30, limit_remaining: -1, limit_reset: 'monthly', usage_monthly: 31 }
|
|
322
|
+
})));
|
|
323
|
+
|
|
324
|
+
const result = await fetchQuota();
|
|
325
|
+
const window = result.usage.windows.monthly;
|
|
326
|
+
|
|
327
|
+
expect(window.usedPercent).toBe(100);
|
|
328
|
+
expect(window.valueLabel).toBe('$31.00 / $30.00');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -323,7 +323,7 @@ const fetchUsage = async (accessToken) => {
|
|
|
323
323
|
'Content-Type': 'application/grpc-web+proto',
|
|
324
324
|
'x-grpc-web': '1',
|
|
325
325
|
'x-user-agent': 'connect-es/2.1.1',
|
|
326
|
-
'User-Agent': '
|
|
326
|
+
'User-Agent': 'AiWorker'
|
|
327
327
|
},
|
|
328
328
|
body: EMPTY_GRPC_WEB_BODY,
|
|
329
329
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
@@ -2,8 +2,10 @@ import express from 'express';
|
|
|
2
2
|
import { deleteManagedCredential, getManagedCredentialStatus, normalizers, readManagedCredential, writeManagedCredential } from './credentials/providers.js';
|
|
3
3
|
import { fetchOllamaCloudUsage } from './providers/ollama-cloud.js';
|
|
4
4
|
import { importCursorCredential, validateCursorCredential } from './providers/cursor.js';
|
|
5
|
+
import { fetchExeDevUsage } from './providers/exe-dev.js';
|
|
5
6
|
|
|
6
7
|
const validators = {
|
|
8
|
+
'exe-dev': fetchExeDevUsage,
|
|
7
9
|
'ollama-cloud': fetchOllamaCloudUsage,
|
|
8
10
|
cursor: validateCursorCredential,
|
|
9
11
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { OPENCODE_CONFIG_DIR } from '../../opencode/shared.js';
|
|
4
5
|
|
|
5
|
-
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
6
6
|
const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
7
7
|
|
|
8
8
|
export const ANTIGRAVITY_ACCOUNTS_PATHS = [
|
|
@@ -25,6 +25,7 @@ Host side (`packages/web/server/lib/relay/`):
|
|
|
25
25
|
- `host-client.js` — the long-lived connection manager: one outbound control connection to the relay, a per-client data connection for each connected device, reconnect/backoff, and the E2EE responder handshake per connection.
|
|
26
26
|
- `host-lock.js` — the per-machine host claim. Every local instance sharing the data dir shares the relay identity (same serverId), so concurrent relay hosts evict each other at the relay worker (`4001: Control replaced`) and paired devices land on whichever local process won last. The claim file (`<data-dir>/relay-host.lock`, `{ pid }`) makes this deterministic: `service.js` only starts the host when no LIVE process holds the claim (stale claims from dead pids are ignored), goes to `standby` otherwise, and a 30s watcher both takes over when the claimant dies and stands down when another process claims. A standby watcher waits a 2-minute grace after the claim frees before taking over, so a cleanly restarting host (app update/relaunch) — which reclaims at boot with no wait — always wins the restart window over a bystander instance. Explicit user intent — creating a pairing link or hitting `/relay/enable` — force-claims; the previous holder's watcher sees the takeover and backs off instead of fighting. Instances created with `allowPassiveHost: false` (dev servers via `AIWORKER_RELAY_HOST=off`, the Electron dev shell via `AIWORKER_ELECTRON_DEV`; `AIWORKER_RELAY_HOST=on` overrides) never start the host passively at all — boot, demand reconcile, and watcher takeover leave them in `standby`; only explicit enable/pairing hosts there. The claim is cooperative (the relay worker still enforces the single host slot); it only decides which process keeps retrying.
|
|
27
27
|
- `tunnel-host.js` — the per-connection dispatcher: decrypts tunnel frames and forwards HTTP/SSE/WS to the local server over loopback, then streams responses back. Enforces a path allowlist and never injects credentials.
|
|
28
|
+
- `downstream-scheduler.js` owns end-client delivery credit and round-robin transmission across response streams. It selects plaintext frames before encryption.
|
|
28
29
|
- `e2ee.js`, `tunnel-codec.js` — host-side (JS) mirrors of the shared crypto and framing (see "Two implementations" below).
|
|
29
30
|
|
|
30
31
|
Client side (`packages/ui/src/lib/relay/`):
|
|
@@ -41,12 +42,59 @@ Relay is not a separate link format: it is one transport candidate inside the un
|
|
|
41
42
|
Everything a client normally sends to the single AiWorker origin:
|
|
42
43
|
- **HTTP** — REST endpoints and proxied OpenCode SDK calls under `/api/*`, plus `/auth/*` and `/health`.
|
|
43
44
|
- **SSE** — long-lived streamed responses (the event stream and notifications). These are just HTTP responses whose body streams; the tunnel needs no special SSE handling.
|
|
44
|
-
- **WebSocket** — the endpoints that use a real socket (the global event stream on platforms that support WS, terminal I/O, dictation).
|
|
45
|
+
- **WebSocket** — the endpoints that use a real socket (the global event stream on platforms that support WS, terminal I/O, dictation, and desktop dev-server previews).
|
|
45
46
|
|
|
46
47
|
The host dispatcher restricts tunneled traffic to explicit path allowlists (one for HTTP, one for WS).
|
|
47
48
|
|
|
48
49
|
Request bodies crossing the tunnel are buffered on the host and forwarded to loopback only once the client's `StreamEnd` frame arrives (bodies above ~512 KB stream live instead). A body whose frames were lost in transit therefore never reaches the loopback server as an empty/truncated chunked body — the host aborts the stream and the client sees an ambiguous transport failure it can retry, instead of the loopback server's bare `400` ("Failed to send message (400)" from the mobile app).
|
|
49
50
|
|
|
51
|
+
## Downstream flow control
|
|
52
|
+
|
|
53
|
+
`hello` and `ready` negotiate `flowControl: true` independently of `batch`.
|
|
54
|
+
When either peer omits the flag, the host keeps the legacy transmission path.
|
|
55
|
+
This capability controls host-to-client traffic only. Uploads retain their
|
|
56
|
+
existing request-body behavior. The Cloudflare broker needs no changes.
|
|
57
|
+
|
|
58
|
+
The client sends encrypted `DeliveryAck` frames on stream zero. Their payload is
|
|
59
|
+
an eight-byte unsigned big-endian cumulative count of received raw tunnel-frame
|
|
60
|
+
bytes on nonzero streams, including the five-byte frame header. Batch envelopes,
|
|
61
|
+
encryption overhead and stream-zero keepalives are excluded. The count resets
|
|
62
|
+
with each handshake. ACKs cover complete frames and include late frames for
|
|
63
|
+
cancelled streams, since those frames still consumed credit. The host rejects
|
|
64
|
+
duplicate, regressing, partial-frame or beyond-sent acknowledgements.
|
|
65
|
+
|
|
66
|
+
The client acknowledges after decoding and dispatching transport data, without
|
|
67
|
+
waiting for React rendering. It coalesces acknowledgements by byte count with a
|
|
68
|
+
short timer for the tail. Keepalives and ACK processing never await downstream
|
|
69
|
+
credit, which would deadlock the channel.
|
|
70
|
+
|
|
71
|
+
The scheduler starts with a small byte window. A backlogged sender can grow its
|
|
72
|
+
window when ACK latency stays near the best observed round trip, and reduces it
|
|
73
|
+
when delivery delay rises. Sparse traffic cannot inflate the window for a later
|
|
74
|
+
bulk burst. The window has a hard upper bound; a sudden bandwidth drop can still
|
|
75
|
+
delay bytes already sent, but cannot create an unlimited network backlog.
|
|
76
|
+
|
|
77
|
+
HTTP bodies and WS messages are sliced into small frames. The scheduler rotates
|
|
78
|
+
streams, preserves each stream's order, and batches selected frames before the
|
|
79
|
+
serialized encryption step. Producers offer a bounded group of slices and await
|
|
80
|
+
transmission before reading more. The legacy timed batcher remains in use only
|
|
81
|
+
without negotiated flow control.
|
|
82
|
+
|
|
83
|
+
HTTP/SSE backpressure reaches the loopback response reader. Node's `ws` pauses
|
|
84
|
+
loopback socket reads while output is waiting. Bun's `ws` shim cannot pause, so
|
|
85
|
+
the dispatcher instead enforces a connection-wide pending WS byte/message cap
|
|
86
|
+
and explicitly aborts the offending substream on overflow. It never silently
|
|
87
|
+
discards deltas. WS output messages are serialized through their last fragment,
|
|
88
|
+
and the close frame follows pending output. Cancellation releases queued work;
|
|
89
|
+
channel teardown releases all producers and acknowledgement state.
|
|
90
|
+
|
|
91
|
+
Regression coverage lives in `downstream-scheduler.test.js`, `flow-control.test.js`
|
|
92
|
+
and `cross-compat.test.js`. The end-to-end fixture uses the real client, host,
|
|
93
|
+
crypto and loopback requests with a byte-paced relay leg. It compares delivery
|
|
94
|
+
ordering, queue growth and a concurrent small response, including both legacy
|
|
95
|
+
fallback directions, unbatched operation, cancellation and fragmented WS output.
|
|
96
|
+
It does not replace a physical iOS/LTE check.
|
|
97
|
+
|
|
50
98
|
## Authentication model
|
|
51
99
|
|
|
52
100
|
- The tunnel is **transport only**. The AiWorker server still authenticates every tunneled request exactly as it authenticates a direct remote client. The relay path grants reachability, not authorization.
|
|
@@ -87,6 +135,13 @@ The E2EE and framing logic exists twice: TypeScript in `packages/ui/src/lib/rela
|
|
|
87
135
|
|
|
88
136
|
## Runtime integration (client)
|
|
89
137
|
|
|
138
|
+
Client keepalive liveness comes from received frames only. Outbound HTTP retries
|
|
139
|
+
cannot suppress a probe of a silent peer. Any valid inbound traffic, including a
|
|
140
|
+
Pong, clears the probe deadline. Terminal relay close codes retain their error
|
|
141
|
+
for the lifetime of that client: subsequent HTTP requests and WS opens fail
|
|
142
|
+
immediately rather than waiting for a reconnect that will never be scheduled.
|
|
143
|
+
Transient failures still use the existing reconnect/backoff path.
|
|
144
|
+
|
|
90
145
|
Relay mode plugs into the existing client transport layer rather than a parallel path: `runtime-switch` activates the tunnel singleton, `runtime-fetch` routes runtime requests through it, `runtime-url`/`runtime-socket` yield tunnel-backed URLs and sockets, and `runtime-auth` mints the URL-scoped token through the tunnel. Direct-URL connections and the Electron realtime-proxy path are unaffected.
|
|
91
146
|
|
|
92
147
|
## Design invariants (do not regress)
|
|
@@ -12,16 +12,42 @@ import {
|
|
|
12
12
|
decodeTunnelFrame as jsDecode,
|
|
13
13
|
encodeFrameBatch as jsEncodeBatch,
|
|
14
14
|
encodeTunnelFrame as jsEncode,
|
|
15
|
+
decodeDeliveryAck,
|
|
15
16
|
} from './tunnel-codec.js';
|
|
16
17
|
import {
|
|
17
18
|
decodeFrameBatch as tsDecodeBatch,
|
|
18
19
|
decodeTunnelFrame as tsDecode,
|
|
19
20
|
encodeFrameBatch as tsEncodeBatch,
|
|
20
21
|
encodeTunnelFrame as tsEncode,
|
|
22
|
+
encodeDeliveryAck,
|
|
21
23
|
} from '../../../../ui/src/lib/relay/tunnel-codec.ts';
|
|
22
24
|
import { TunnelFrameType as TsFrameType } from '../../../../ui/src/lib/relay/protocol.ts';
|
|
23
25
|
|
|
24
26
|
describe('relay JS-host <-> TS-client cross compatibility', () => {
|
|
27
|
+
it('negotiates flow control independently of batching, with legacy fallback on either side', async () => {
|
|
28
|
+
const keys = await generateEcdhKeyPair();
|
|
29
|
+
const pub = await exportPublicKeyJwk(keys.publicKey);
|
|
30
|
+
for (const batch of [false, true]) {
|
|
31
|
+
for (const clientFlow of [false, true]) {
|
|
32
|
+
for (const hostFlow of [false, true]) {
|
|
33
|
+
const client = await createClientHandshake(pub, { batch, flowControl: clientFlow });
|
|
34
|
+
const host = createHostHandshake(keys.privateKey, { batch, flowControl: hostFlow });
|
|
35
|
+
const ready = await host.handleText(client.helloText);
|
|
36
|
+
const established = await client.handleText(ready.replyText);
|
|
37
|
+
expect(ready.flowControl).toBe(clientFlow && hostFlow);
|
|
38
|
+
expect(established.flowControl).toBe(clientFlow && hostFlow);
|
|
39
|
+
expect(established.batch).toBe(batch);
|
|
40
|
+
expect((await host.handleText(client.helloText)).text).toBe(ready.replyText);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
expect(JsFrameType).toEqual(TsFrameType);
|
|
45
|
+
for (const bytes of [0, 8197, 2 ** 32 + 17, Number.MAX_SAFE_INTEGER]) {
|
|
46
|
+
expect(decodeDeliveryAck(encodeDeliveryAck(bytes))).toBe(bytes);
|
|
47
|
+
}
|
|
48
|
+
expect(() => decodeDeliveryAck(new Uint8Array(7))).toThrow();
|
|
49
|
+
expect(() => decodeDeliveryAck(new Uint8Array(8).fill(255))).toThrow();
|
|
50
|
+
});
|
|
25
51
|
it('completes a handshake and exchanges frames both ways', async () => {
|
|
26
52
|
const hostKeys = await generateEcdhKeyPair();
|
|
27
53
|
const hostPubJwk = await exportPublicKeyJwk(hostKeys.publicKey);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Downstream credit belongs to the end client, not the relay's TCP connection.
|
|
2
|
+
// Producers await send(), so HTTP readers stop before building a plaintext queue.
|
|
3
|
+
import { decodeTunnelFrame } from './tunnel-codec.js';
|
|
4
|
+
import { MAX_PLAINTEXT_FRAME_BYTES } from './e2ee.js';
|
|
5
|
+
|
|
6
|
+
export const DOWNSTREAM_CHUNK_BYTES = 8 * 1024;
|
|
7
|
+
const MIN_WINDOW_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_WINDOW_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_QUEUED_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
const MAX_QUEUED_FRAMES = 4096;
|
|
11
|
+
const MAX_OUTSTANDING_FRAMES = 4096;
|
|
12
|
+
const QUEUE_DELAY_TARGET_MS = 100;
|
|
13
|
+
|
|
14
|
+
export const createDownstreamScheduler = ({ sendBatch, onError, maxBatchFrames = 32, now = () => performance.now() }) => {
|
|
15
|
+
const queues = new Map();
|
|
16
|
+
const outstanding = [];
|
|
17
|
+
let queuedBytes = 0;
|
|
18
|
+
let queuedFrames = 0;
|
|
19
|
+
let sentBytes = 0;
|
|
20
|
+
let acknowledgedBytes = 0;
|
|
21
|
+
let windowBytes = MIN_WINDOW_BYTES;
|
|
22
|
+
let minRtt = Infinity;
|
|
23
|
+
let adjustmentBoundary = MIN_WINDOW_BYTES;
|
|
24
|
+
let windowLimited = false;
|
|
25
|
+
let draining = false;
|
|
26
|
+
let closed = false;
|
|
27
|
+
let sending = [];
|
|
28
|
+
|
|
29
|
+
const close = () => {
|
|
30
|
+
closed = true;
|
|
31
|
+
for (const queue of queues.values()) for (const entry of queue) entry.resolve();
|
|
32
|
+
for (const entry of sending) entry.resolve();
|
|
33
|
+
sending = [];
|
|
34
|
+
queues.clear();
|
|
35
|
+
outstanding.length = 0;
|
|
36
|
+
queuedBytes = 0;
|
|
37
|
+
queuedFrames = 0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const fail = error => {
|
|
41
|
+
close();
|
|
42
|
+
onError(error);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const drain = async () => {
|
|
46
|
+
if (draining || closed) return;
|
|
47
|
+
draining = true;
|
|
48
|
+
try {
|
|
49
|
+
while (!closed) {
|
|
50
|
+
const selected = [];
|
|
51
|
+
let batchBytes = 1;
|
|
52
|
+
while (selected.length < maxBatchFrames) {
|
|
53
|
+
// Stream zero carries only tiny keepalives; it must not wait for credit.
|
|
54
|
+
const next = queues.has(0) ? [0, queues.get(0)] : queues.entries().next().value;
|
|
55
|
+
if (!next) break;
|
|
56
|
+
const [streamId, queue] = next;
|
|
57
|
+
const entry = queue[0];
|
|
58
|
+
// Do not skip a larger head indefinitely in favour of smaller frames.
|
|
59
|
+
if (streamId !== 0 && sentBytes - acknowledgedBytes + entry.frame.length > windowBytes) {
|
|
60
|
+
windowLimited = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (streamId !== 0 && outstanding.length >= MAX_OUTSTANDING_FRAMES) break;
|
|
64
|
+
if (batchBytes + 4 + entry.frame.length > MAX_PLAINTEXT_FRAME_BYTES) break;
|
|
65
|
+
queue.shift();
|
|
66
|
+
queues.delete(streamId);
|
|
67
|
+
if (queue.length) queues.set(streamId, queue);
|
|
68
|
+
queuedBytes -= entry.frame.length;
|
|
69
|
+
queuedFrames -= 1;
|
|
70
|
+
if (streamId !== 0) {
|
|
71
|
+
sentBytes += entry.frame.length;
|
|
72
|
+
outstanding.push({ end: sentBytes, at: now() });
|
|
73
|
+
}
|
|
74
|
+
selected.push(entry);
|
|
75
|
+
batchBytes += 4 + entry.frame.length;
|
|
76
|
+
}
|
|
77
|
+
if (!selected.length) break;
|
|
78
|
+
sending = selected;
|
|
79
|
+
try {
|
|
80
|
+
// Selection precedes encryption. Never reorder encrypted IV counters.
|
|
81
|
+
await sendBatch(selected.map(entry => entry.frame));
|
|
82
|
+
} finally {
|
|
83
|
+
for (const entry of selected) entry.resolve();
|
|
84
|
+
sending = [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
fail(error);
|
|
89
|
+
} finally {
|
|
90
|
+
draining = false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
send(frame) {
|
|
96
|
+
if (closed) return Promise.resolve();
|
|
97
|
+
const { streamId } = decodeTunnelFrame(frame);
|
|
98
|
+
if (queuedBytes + frame.length > MAX_QUEUED_BYTES || queuedFrames >= MAX_QUEUED_FRAMES) {
|
|
99
|
+
fail(new Error('relay downstream queue limit exceeded'));
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
const queue = queues.get(streamId) ?? [];
|
|
104
|
+
queue.push({ frame, resolve, streamId });
|
|
105
|
+
queues.set(streamId, queue);
|
|
106
|
+
queuedBytes += frame.length;
|
|
107
|
+
queuedFrames += 1;
|
|
108
|
+
// Let concurrent streams join this round before selecting the next frame.
|
|
109
|
+
queueMicrotask(() => { void drain(); });
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
acknowledge(bytes) {
|
|
113
|
+
if (closed) return;
|
|
114
|
+
if (!Number.isSafeInteger(bytes) || bytes <= acknowledgedBytes || bytes > sentBytes) {
|
|
115
|
+
throw new Error('invalid relay delivery acknowledgement');
|
|
116
|
+
}
|
|
117
|
+
// An ACK may cover several complete frames, never a partial frame.
|
|
118
|
+
const endIndex = outstanding.findIndex(entry => entry.end === bytes);
|
|
119
|
+
if (endIndex < 0) throw new Error('relay acknowledgement is not a frame boundary');
|
|
120
|
+
const rtt = Math.max(1, now() - outstanding[endIndex].at);
|
|
121
|
+
outstanding.splice(0, endIndex + 1);
|
|
122
|
+
acknowledgedBytes = bytes;
|
|
123
|
+
minRtt = Math.min(minRtt, rtt);
|
|
124
|
+
// Grow on a clear path, reduce when the client's delivery delay grows.
|
|
125
|
+
// Adjust once per window, not once per frame in a burst of ACKs.
|
|
126
|
+
if (bytes >= adjustmentBoundary) {
|
|
127
|
+
if (rtt > minRtt + QUEUE_DELAY_TARGET_MS) {
|
|
128
|
+
windowBytes = Math.max(MIN_WINDOW_BYTES, Math.floor(windowBytes / 2));
|
|
129
|
+
} else if (windowLimited) {
|
|
130
|
+
// Sparse token traffic must not inflate a future bash burst's window.
|
|
131
|
+
windowBytes = Math.min(MAX_WINDOW_BYTES, windowBytes * 2);
|
|
132
|
+
}
|
|
133
|
+
windowLimited = false;
|
|
134
|
+
adjustmentBoundary = bytes + windowBytes;
|
|
135
|
+
}
|
|
136
|
+
void drain();
|
|
137
|
+
},
|
|
138
|
+
cancel(streamId) {
|
|
139
|
+
// Already-selected bytes may still arrive and must still be ACKed, but
|
|
140
|
+
// cancelled producers need not wait for an in-progress encryption call.
|
|
141
|
+
for (const entry of sending) if (entry.streamId === streamId) entry.resolve();
|
|
142
|
+
const queue = queues.get(streamId);
|
|
143
|
+
if (!queue) return;
|
|
144
|
+
queues.delete(streamId);
|
|
145
|
+
for (const entry of queue) {
|
|
146
|
+
queuedBytes -= entry.frame.length;
|
|
147
|
+
queuedFrames -= 1;
|
|
148
|
+
entry.resolve();
|
|
149
|
+
}
|
|
150
|
+
void drain();
|
|
151
|
+
},
|
|
152
|
+
close,
|
|
153
|
+
};
|
|
154
|
+
};
|