@aiworker/web 1.22.0 → 1.22.2
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 +7 -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-CSICpWQD.js} +1 -1
- package/dist/assets/{DiffView-DluiRSla.js → DiffView-hDBKesA_.js} +3 -3
- package/dist/assets/DirtyBranchSwitchDialog-C22HdWj4.js +1 -0
- package/dist/assets/FilesView-CdWzeOp9.js +17 -0
- package/dist/assets/GitView-VQQkYuUw.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-6lrhwD3q.js} +1 -1
- package/dist/assets/LinearIssuesView-Ba6Q9569.js +1 -0
- package/dist/assets/{MarkdownImageGallery-ClOvuZxC.js → MarkdownImageGallery-Czv_mhvS.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +12 -0
- package/dist/assets/{OnboardingScreen-D2DG7K6b.js → OnboardingScreen-ChxQoAkx.js} +1 -1
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-BzcZPlOq.js} +1 -1
- package/dist/assets/{PierreDiffViewer-B7m3f0d7.js → PierreDiffViewer-BchBtZlp.js} +2 -2
- package/dist/assets/PlanView-DyDt_nZ_.js +5 -0
- package/dist/assets/SettingsView-BKf5xBsP.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-DDQYaKWQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-iYF8Y2bz.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-CdjbOVMM.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-fFFm2QgH.js} +1 -1
- package/dist/assets/{WalkthroughView-egQgxel3.js → WalkthroughView-DOeCsMju.js} +2 -2
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-CzgLum_K.js} +2 -2
- package/dist/assets/{contextPanelEmbeddedChat-C3fjDbdg.js → contextPanelEmbeddedChat-C0ZLAepL.js} +1 -1
- package/dist/assets/{de-C1P6X5Rs.js → de-B5JaA3SA.js} +6 -5
- package/dist/assets/{es-CDgaF1_t.js → es-EPIH88ii.js} +6 -5
- package/dist/assets/{fr-Bgxqvvz_.js → fr-CAMfxUdZ.js} +6 -5
- package/dist/assets/{index-CODwDsU_.js → index-Bl3LSbjn.js} +1 -1
- package/dist/assets/index-DF5kMP-i.js +39 -0
- package/dist/assets/index-Dl3xbAB_.css +1 -0
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-Dx-dB_8l.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-CWFPwmnU.js} +1 -1
- package/dist/assets/{ja-BDRrEcNP.js → ja-CifOE1VN.js} +7 -6
- package/dist/assets/{ko-Ba8sQV79.js → ko-BBDyAyYQ.js} +7 -6
- package/dist/assets/{main-C5DVOxc6.js → main-DnBAoktx.js} +2 -2
- package/dist/assets/main-MhSaOZoB.js +930 -0
- package/dist/assets/{markdownCore-CZwzz5gw.js → markdownCore-BrBwOOMn.js} +2 -2
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-BKyvLseN.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-DKjeFgMN.js} +2 -2
- package/dist/assets/{patchFileDiff-yNfWXN4E.js → patchFileDiff-o8ms1me8.js} +1 -1
- package/dist/assets/pl-DgEHYfFM.js +16 -0
- package/dist/assets/{pt-BR-DIs6ElwV.js → pt-BR-DuWHojUw.js} +6 -5
- package/dist/assets/{renderElectronMiniChatApp-BcKLHXKe.js → renderElectronMiniChatApp-CDgBwwCS.js} +2 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +25 -0
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +17 -0
- package/dist/assets/{scheduledTasksApi-DLW0mkQK.js → scheduledTasksApi-D8yamh1b.js} +1 -1
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-TEQCmzW8.js} +1 -1
- package/dist/assets/tr-D5m2yIWL.js +7 -0
- package/dist/assets/{uk-ZrCNjM7Y.js → uk-D_q60oog.js} +6 -5
- package/dist/assets/{useAppFontEffects-CjrntdL0.js → useAppFontEffects-D7sNpuL2.js} +161 -190
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +2 -0
- package/dist/assets/{useRootScrollLock-BHYPkrLv.js → useRootScrollLock-lXEnWlaU.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Dp3KMRQG.js} +2 -2
- package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-_mNEMjFY.js} +1 -1
- package/dist/assets/{zh-CN-iv_AV4EC.js → zh-CN-D8QvsVeg.js} +6 -5
- package/dist/assets/{zh-TW-CzEUk38j.js → zh-TW-B9t7rQQO.js} +6 -5
- package/dist/index.html +22 -22
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.js +118 -85
- 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/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 +6 -6
- package/server/lib/fs/DOCUMENTATION.md +2 -0
- package/server/lib/fs/routes.js +37 -26
- package/server/lib/fs/routes.test.js +98 -21
- package/server/lib/git/DOCUMENTATION.md +3 -1
- 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 +17 -1
- package/server/lib/git/service.js +116 -26
- package/server/lib/git/service.test.js +174 -3
- 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 +1 -1
- package/server/lib/inherited-env.test.js +2 -2
- 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 +158 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -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 +13 -5
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +6 -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 +17 -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 +15 -15
- package/server/lib/opencode/openchamber-routes.test.js +11 -11
- 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 +14 -14
- 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-helpers.js +31 -3
- package/server/lib/opencode/settings-helpers.test.js +189 -12
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/opencode/settings-runtime.test.js +29 -0
- 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/routes.js +1 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +9 -1
- 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/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 +15 -0
- package/server/lib/quota/providers/ollama-cloud.js +1 -1
- package/server/lib/quota/providers/opencode-go.js +2 -1
- package/server/lib/quota/providers/opencode-go.test.js +9 -5
- 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 +1 -1
- package/server/lib/relay/host-client.js +1 -1
- 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-host.js +3 -1
- 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 +10 -9
- 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 +88 -22
- 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 +4 -0
- package/server/lib/small-model/call.js +9 -3
- package/server/lib/small-model/call.test.js +29 -6
- package/server/lib/small-model/index.js +8 -7
- 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/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +11 -7
- package/server/lib/terminal/runtime.js +180 -26
- package/server/lib/terminal/runtime.test.js +554 -27
- 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/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 +3 -3
- package/server/lib/walkthrough/model-settings.test.js +1 -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/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/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/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- 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/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- 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,75 @@
|
|
|
1
|
+
import { readManagedCredential } from '../credentials/providers.js';
|
|
2
|
+
import { asObject, buildResult, formatMoney, toNumber, toTimestamp, toUsageWindow } from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
export const providerId = 'exe-dev';
|
|
5
|
+
export const providerName = 'exe.dev';
|
|
6
|
+
export const aliases = ['exe-dev'];
|
|
7
|
+
const EXEC_URL = 'https://exe.dev/exec';
|
|
8
|
+
const USAGE_COMMAND = 'billing credits usage --group=day --json';
|
|
9
|
+
|
|
10
|
+
export const parseExeDevUsage = (payload) => {
|
|
11
|
+
const data = asObject(payload);
|
|
12
|
+
if (!data) return null;
|
|
13
|
+
|
|
14
|
+
const totalCost = toNumber(data.total_cost_usd);
|
|
15
|
+
const monthlyAllowance = toNumber(data.monthly_allowance_usd);
|
|
16
|
+
const resetAt = toTimestamp(data.period_end);
|
|
17
|
+
if (totalCost === null || monthlyAllowance === null || monthlyAllowance < 0 || resetAt === null) return null;
|
|
18
|
+
|
|
19
|
+
const usedPercent = monthlyAllowance > 0
|
|
20
|
+
? Math.min(100, Math.max(0, (totalCost / monthlyAllowance) * 100))
|
|
21
|
+
: null;
|
|
22
|
+
const spent = formatMoney(totalCost);
|
|
23
|
+
const allowance = formatMoney(monthlyAllowance);
|
|
24
|
+
if (spent === null || allowance === null) return null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
monthly: toUsageWindow({
|
|
28
|
+
usedPercent,
|
|
29
|
+
windowSeconds: null,
|
|
30
|
+
resetAt,
|
|
31
|
+
valueLabel: `$${spent} / $${allowance}`,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const fetchExeDevUsage = async (credential, fetchImpl = fetch) => {
|
|
37
|
+
const response = await fetchImpl(EXEC_URL, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
Accept: 'application/json',
|
|
41
|
+
Authorization: `Bearer ${credential.usageToken}`,
|
|
42
|
+
'Content-Type': 'text/plain',
|
|
43
|
+
'User-Agent': 'AiWorker quota provider',
|
|
44
|
+
},
|
|
45
|
+
body: USAGE_COMMAND,
|
|
46
|
+
signal: AbortSignal.timeout(15_000),
|
|
47
|
+
});
|
|
48
|
+
if (response.status === 401 || response.status === 403) throw new Error('exe.dev authentication failed');
|
|
49
|
+
if (!response.ok) throw new Error(`exe.dev usage API returned HTTP ${response.status}`);
|
|
50
|
+
const windows = parseExeDevUsage(await response.json().catch(() => null));
|
|
51
|
+
if (!windows) throw new Error('exe.dev usage data could not be parsed');
|
|
52
|
+
return windows;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const isConfigured = () => Boolean(readManagedCredential(providerId));
|
|
56
|
+
|
|
57
|
+
export const fetchQuota = async () => {
|
|
58
|
+
const credential = readManagedCredential(providerId);
|
|
59
|
+
if (!credential) {
|
|
60
|
+
return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const windows = await fetchExeDevUsage(credential);
|
|
65
|
+
return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows } });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return buildResult({
|
|
68
|
+
providerId,
|
|
69
|
+
providerName,
|
|
70
|
+
ok: false,
|
|
71
|
+
configured: true,
|
|
72
|
+
error: error instanceof Error ? error.message : 'Request failed',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { fetchExeDevUsage, parseExeDevUsage } from './exe-dev.js';
|
|
3
|
+
|
|
4
|
+
const payload = {
|
|
5
|
+
allowance_spend_usd: 0.11,
|
|
6
|
+
extra_spend_usd: 0,
|
|
7
|
+
group: 'day',
|
|
8
|
+
month: '2026-09',
|
|
9
|
+
monthly_allowance_usd: 20,
|
|
10
|
+
period_end: '2026-10-01T00:00:00Z',
|
|
11
|
+
period_start: '2026-09-01T00:00:00Z',
|
|
12
|
+
total_cost_usd: 0.11,
|
|
13
|
+
total_requests: 17,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('exe.dev quota provider', () => {
|
|
17
|
+
it('parses monthly credit usage', () => {
|
|
18
|
+
const windows = parseExeDevUsage(payload);
|
|
19
|
+
expect(windows?.monthly.usedPercent).toBeCloseTo(0.55);
|
|
20
|
+
expect(windows?.monthly.valueLabel).toBe('$0.11 / $20.00');
|
|
21
|
+
expect(windows?.monthly.resetAt).toBe(Date.parse('2026-10-01T00:00:00Z'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('sends the scoped billing command without exposing the token elsewhere', async () => {
|
|
25
|
+
const requests = [];
|
|
26
|
+
const windows = await fetchExeDevUsage({ usageToken: 'test-token' }, async (url, init) => {
|
|
27
|
+
requests.push({ url, init });
|
|
28
|
+
return Response.json(payload);
|
|
29
|
+
});
|
|
30
|
+
expect(windows.monthly.valueLabel).toBe('$0.11 / $20.00');
|
|
31
|
+
expect(requests).toHaveLength(1);
|
|
32
|
+
expect(requests[0].url).toBe('https://exe.dev/exec');
|
|
33
|
+
expect(requests[0].init.method).toBe('POST');
|
|
34
|
+
expect(requests[0].init.body).toBe('billing credits usage --group=day --json');
|
|
35
|
+
expect(requests[0].init.headers.Authorization).toBe('Bearer test-token');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects malformed successful responses', async () => {
|
|
39
|
+
await expect(fetchExeDevUsage({ usageToken: 'test-token' }, async () => Response.json({})))
|
|
40
|
+
.rejects.toThrow('could not be parsed');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reports authentication failure without including the token', async () => {
|
|
44
|
+
await expect(fetchExeDevUsage({ usageToken: 'test-token' }, async () => new Response('', { status: 401 })))
|
|
45
|
+
.rejects.toThrow('exe.dev authentication failed');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import {
|
|
3
|
+
getAuthEntry,
|
|
4
|
+
normalizeAuthEntry,
|
|
5
|
+
buildResult,
|
|
6
|
+
toUsageWindow,
|
|
7
|
+
toNumber,
|
|
8
|
+
formatMoney,
|
|
9
|
+
asObject,
|
|
10
|
+
asNonEmptyString
|
|
11
|
+
} from '../utils/index.js';
|
|
12
|
+
|
|
13
|
+
export const providerId = 'hyper';
|
|
14
|
+
export const providerName = 'Charm Hyper';
|
|
15
|
+
export const aliases = ['hyper'];
|
|
16
|
+
const HYPER_QUOTA_URL = 'https://hyper.charm.land/v1/credits';
|
|
17
|
+
const CREDIT_TO_USD = 0.05;
|
|
18
|
+
|
|
19
|
+
const getApiKey = (auth) => {
|
|
20
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
21
|
+
return asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const isConfigured = (auth = readAuthFile()) => Boolean(getApiKey(auth));
|
|
25
|
+
|
|
26
|
+
export const fetchQuota = async ({ readAuth = readAuthFile, fetchImpl = fetch } = {}) => {
|
|
27
|
+
const apiKey = getApiKey(readAuth());
|
|
28
|
+
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
return buildResult({
|
|
31
|
+
providerId,
|
|
32
|
+
providerName,
|
|
33
|
+
ok: false,
|
|
34
|
+
configured: false,
|
|
35
|
+
error: 'Not configured'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetchImpl(HYPER_QUOTA_URL, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${apiKey}`,
|
|
46
|
+
'Accept-Encoding': 'identity'
|
|
47
|
+
},
|
|
48
|
+
signal: timeoutSignal
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
return buildResult({
|
|
53
|
+
providerId,
|
|
54
|
+
providerName,
|
|
55
|
+
ok: false,
|
|
56
|
+
configured: true,
|
|
57
|
+
error: response.status === 401 || response.status === 403
|
|
58
|
+
? 'Session expired — please re-authenticate with Charm Hyper'
|
|
59
|
+
: `API error: ${response.status}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const payload = asObject(await response.json());
|
|
64
|
+
const rawBalance = payload?.balance;
|
|
65
|
+
const balance = toNumber(asNonEmptyString(rawBalance)
|
|
66
|
+
?? (Number.isFinite(rawBalance) ? rawBalance : null));
|
|
67
|
+
|
|
68
|
+
if (balance === null) {
|
|
69
|
+
return buildResult({
|
|
70
|
+
providerId,
|
|
71
|
+
providerName,
|
|
72
|
+
ok: false,
|
|
73
|
+
configured: true,
|
|
74
|
+
error: 'No quota data in response'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const creditsLabel = Number.isInteger(balance) ? String(balance) : formatMoney(balance);
|
|
79
|
+
const windows = {
|
|
80
|
+
credits_balance: toUsageWindow({
|
|
81
|
+
usedPercent: null,
|
|
82
|
+
windowSeconds: null,
|
|
83
|
+
resetAt: null,
|
|
84
|
+
valueLabel: `$${formatMoney(balance * CREDIT_TO_USD)}`
|
|
85
|
+
}),
|
|
86
|
+
credits: toUsageWindow({
|
|
87
|
+
usedPercent: null,
|
|
88
|
+
windowSeconds: null,
|
|
89
|
+
resetAt: null,
|
|
90
|
+
valueLabel: creditsLabel
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return buildResult({
|
|
95
|
+
providerId,
|
|
96
|
+
providerName,
|
|
97
|
+
ok: true,
|
|
98
|
+
configured: true,
|
|
99
|
+
usage: { windows }
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const isTimeout = error instanceof DOMException && (
|
|
103
|
+
error.name === 'TimeoutError' || (error.name === 'AbortError' && timeoutSignal.aborted)
|
|
104
|
+
);
|
|
105
|
+
const isParseError = error instanceof SyntaxError;
|
|
106
|
+
return buildResult({
|
|
107
|
+
providerId,
|
|
108
|
+
providerName,
|
|
109
|
+
ok: false,
|
|
110
|
+
configured: true,
|
|
111
|
+
error: isTimeout
|
|
112
|
+
? 'Request timed out'
|
|
113
|
+
: isParseError
|
|
114
|
+
? 'Invalid response from provider'
|
|
115
|
+
: (error instanceof Error ? error.message : 'Request failed')
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { fetchQuota, isConfigured } from './hyper.js';
|
|
3
|
+
|
|
4
|
+
const readAuth = () => ({ hyper: { key: 'test-token' } });
|
|
5
|
+
|
|
6
|
+
// https://hyper.charm.land/docs/api/credits.html documents the balance payload.
|
|
7
|
+
// https://hyper.charm.land/faq defines one Hypercredit as $0.05.
|
|
8
|
+
describe('Charm Hyper quota provider', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
[100, '100', '$5.00'],
|
|
11
|
+
['50', '50', '$2.50'],
|
|
12
|
+
[25.5, '25.50', '$1.28'],
|
|
13
|
+
[0, '0', '$0.00'],
|
|
14
|
+
['0', '0', '$0.00'],
|
|
15
|
+
])('formats balance %s without an untranslated unit', async (balance, credits, dollars) => {
|
|
16
|
+
const result = await fetchQuota({
|
|
17
|
+
readAuth,
|
|
18
|
+
fetchImpl: async () => Response.json({ balance }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result.ok).toBe(true);
|
|
22
|
+
expect(result.providerId).toBe('hyper');
|
|
23
|
+
expect(result.configured).toBe(true);
|
|
24
|
+
expect(result.usage.windows.credits.valueLabel).toBe(credits);
|
|
25
|
+
expect(result.usage.windows.credits_balance.valueLabel).toBe(dollars);
|
|
26
|
+
for (const window of Object.values(result.usage.windows)) {
|
|
27
|
+
expect(window.usedPercent).toBeNull();
|
|
28
|
+
expect(window.remainingPercent).toBeNull();
|
|
29
|
+
expect(window.windowSeconds).toBeNull();
|
|
30
|
+
expect(window.resetAt).toBeNull();
|
|
31
|
+
expect(window.resetAfterSeconds).toBeNull();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
{}, null, [], { balance: '' }, { balance: ' \t ' }, { balance: 'NaN' },
|
|
37
|
+
{ balance: 'Infinity' }, { balance: null }, { balance: true }, { balance: [] },
|
|
38
|
+
{ balance: {} },
|
|
39
|
+
])('rejects invalid payload %j instead of showing zero', async (payload) => {
|
|
40
|
+
const result = await fetchQuota({ readAuth, fetchImpl: async () => Response.json(payload) });
|
|
41
|
+
|
|
42
|
+
expect(result.ok).toBe(false);
|
|
43
|
+
expect(result.configured).toBe(true);
|
|
44
|
+
expect(result.error).toBe('No quota data in response');
|
|
45
|
+
expect(result.usage).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it.each([
|
|
49
|
+
{ hyper: { key: 'test-token' } },
|
|
50
|
+
{ hyper: { token: 'test-token' } },
|
|
51
|
+
{ hyper: 'test-token' },
|
|
52
|
+
{ hyper: { key: ' ', token: 'test-token' } },
|
|
53
|
+
{ hyper: { key: 42, token: 'test-token' } },
|
|
54
|
+
])('uses a validated credential for the documented request', async (auth) => {
|
|
55
|
+
expect(isConfigured(auth)).toBe(true);
|
|
56
|
+
let requests = 0;
|
|
57
|
+
const result = await fetchQuota({
|
|
58
|
+
readAuth: () => auth,
|
|
59
|
+
fetchImpl: async (url, options) => {
|
|
60
|
+
requests += 1;
|
|
61
|
+
expect(url).toBe('https://hyper.charm.land/v1/credits');
|
|
62
|
+
expect(options.method).toBe('GET');
|
|
63
|
+
expect(new Headers(options.headers).get('Authorization')).toBe('Bearer test-token');
|
|
64
|
+
expect(options.signal).toBeInstanceOf(AbortSignal);
|
|
65
|
+
return Response.json({ balance: 100 });
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(requests).toBe(1);
|
|
70
|
+
expect(result.ok).toBe(true);
|
|
71
|
+
expect(JSON.stringify(result)).not.toContain('test-token');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each([{}, { hyper: { key: '' } }, { hyper: { key: ' ' } }, { hyper: { key: 42 } }])(
|
|
75
|
+
'does not request usage without a valid credential',
|
|
76
|
+
async (auth) => {
|
|
77
|
+
expect(isConfigured(auth)).toBe(false);
|
|
78
|
+
let requests = 0;
|
|
79
|
+
const result = await fetchQuota({
|
|
80
|
+
readAuth: () => auth,
|
|
81
|
+
fetchImpl: async () => {
|
|
82
|
+
requests += 1;
|
|
83
|
+
return Response.json({ balance: 100 });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(requests).toBe(0);
|
|
88
|
+
expect(result.ok).toBe(false);
|
|
89
|
+
expect(result.configured).toBe(false);
|
|
90
|
+
expect(result.error).toBe('Not configured');
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
[401, 'Session expired — please re-authenticate with Charm Hyper'],
|
|
96
|
+
[403, 'Session expired — please re-authenticate with Charm Hyper'],
|
|
97
|
+
[429, 'API error: 429'],
|
|
98
|
+
[500, 'API error: 500'],
|
|
99
|
+
])('reports HTTP %s as a failure', async (status, error) => {
|
|
100
|
+
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response(null, { status }) });
|
|
101
|
+
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
expect(result.configured).toBe(true);
|
|
104
|
+
expect(result.error).toBe(error);
|
|
105
|
+
expect(result.usage).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('reports invalid JSON as a parse failure', async () => {
|
|
109
|
+
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response('{') });
|
|
110
|
+
expect(result.error).toBe('Invalid response from provider');
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
expect(result.configured).toBe(true);
|
|
113
|
+
expect(result.usage).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it.each([
|
|
117
|
+
[new DOMException('Timed out', 'TimeoutError'), 'Request timed out'],
|
|
118
|
+
[new Error('Network unavailable'), 'Network unavailable'],
|
|
119
|
+
])('reports request failure', async (failure, message) => {
|
|
120
|
+
const result = await fetchQuota({ readAuth, fetchImpl: async () => { throw failure; } });
|
|
121
|
+
expect(result.error).toBe(message);
|
|
122
|
+
expect(result.ok).toBe(false);
|
|
123
|
+
expect(result.configured).toBe(true);
|
|
124
|
+
expect(result.usage).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -13,7 +13,9 @@ import * as copilot from './copilot.js';
|
|
|
13
13
|
import * as crof from './crof.js';
|
|
14
14
|
import * as cursor from './cursor.js';
|
|
15
15
|
import * as deepseek from './deepseek.js';
|
|
16
|
+
import * as exeDev from './exe-dev.js';
|
|
16
17
|
import * as google from './google/index.js';
|
|
18
|
+
import * as hyper from './hyper.js';
|
|
17
19
|
import * as kimi from './kimi.js';
|
|
18
20
|
import * as nanogpt from './nanogpt.js';
|
|
19
21
|
import * as openai from './openai.js';
|
|
@@ -59,12 +61,24 @@ const registry = {
|
|
|
59
61
|
isConfigured: deepseek.isConfigured,
|
|
60
62
|
fetchQuota: deepseek.fetchQuota
|
|
61
63
|
},
|
|
64
|
+
'exe-dev': {
|
|
65
|
+
providerId: exeDev.providerId,
|
|
66
|
+
providerName: exeDev.providerName,
|
|
67
|
+
isConfigured: exeDev.isConfigured,
|
|
68
|
+
fetchQuota: exeDev.fetchQuota
|
|
69
|
+
},
|
|
62
70
|
google: {
|
|
63
71
|
providerId: google.providerId,
|
|
64
72
|
providerName: google.providerName,
|
|
65
73
|
isConfigured: google.isConfigured,
|
|
66
74
|
fetchQuota: google.fetchGoogleQuota
|
|
67
75
|
},
|
|
76
|
+
hyper: {
|
|
77
|
+
providerId: hyper.providerId,
|
|
78
|
+
providerName: hyper.providerName,
|
|
79
|
+
isConfigured: hyper.isConfigured,
|
|
80
|
+
fetchQuota: hyper.fetchQuota
|
|
81
|
+
},
|
|
68
82
|
'zai-coding-plan': {
|
|
69
83
|
providerId: zai.providerId,
|
|
70
84
|
providerName: zai.providerName,
|
|
@@ -213,6 +227,7 @@ export const fetchGoogleQuota = google.fetchGoogleQuota;
|
|
|
213
227
|
export const fetchCodexQuota = codex.fetchQuota;
|
|
214
228
|
export const fetchCursorQuota = cursor.fetchQuota;
|
|
215
229
|
export const fetchDeepseekQuota = deepseek.fetchQuota;
|
|
230
|
+
export const fetchHyperQuota = hyper.fetchQuota;
|
|
216
231
|
export const fetchCopilotQuota = copilot.fetchQuota;
|
|
217
232
|
export const fetchCopilotAddonQuota = copilot.fetchQuotaAddon;
|
|
218
233
|
export const fetchKimiQuota = kimi.fetchQuota;
|
|
@@ -45,7 +45,7 @@ export const isConfigured = () => {
|
|
|
45
45
|
export const fetchOllamaCloudUsage = async (credential, fetchImpl = fetch) => {
|
|
46
46
|
const response = await fetchImpl('https://ollama.com/settings', {
|
|
47
47
|
method: 'GET',
|
|
48
|
-
headers: { Cookie: credential.cookie, 'User-Agent': '
|
|
48
|
+
headers: { Cookie: credential.cookie, 'User-Agent': 'AiWorker quota provider' },
|
|
49
49
|
redirect: 'manual',
|
|
50
50
|
signal: AbortSignal.timeout(15_000),
|
|
51
51
|
});
|
|
@@ -37,7 +37,8 @@ export const fetchOpenCodeGoUsage = async (apiKey, fetchImpl = fetch) => {
|
|
|
37
37
|
headers: {
|
|
38
38
|
Accept: 'application/json',
|
|
39
39
|
Authorization: `Bearer ${apiKey}`,
|
|
40
|
-
'
|
|
40
|
+
'x-opencode-session': 'openchamber-usage',
|
|
41
|
+
'User-Agent': 'AiWorker quota provider',
|
|
41
42
|
},
|
|
42
43
|
signal: AbortSignal.timeout(15_000),
|
|
43
44
|
});
|
|
@@ -3,9 +3,9 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
|
-
const previousDataDirectory = process.env.
|
|
6
|
+
const previousDataDirectory = process.env.AIWORKER_DATA_DIR;
|
|
7
7
|
const temporaryDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-opencode-go-'));
|
|
8
|
-
process.env.
|
|
8
|
+
process.env.AIWORKER_DATA_DIR = temporaryDataDirectory;
|
|
9
9
|
|
|
10
10
|
vi.mock('../../opencode/auth.js', () => ({
|
|
11
11
|
readAuthFile: () => ({ 'opencode-go': { key: 'test-key' } }),
|
|
@@ -18,8 +18,8 @@ afterEach(() => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
afterAll(() => {
|
|
21
|
-
if (previousDataDirectory === undefined) delete process.env.
|
|
22
|
-
else process.env.
|
|
21
|
+
if (previousDataDirectory === undefined) delete process.env.AIWORKER_DATA_DIR;
|
|
22
|
+
else process.env.AIWORKER_DATA_DIR = previousDataDirectory;
|
|
23
23
|
fs.rmSync(temporaryDataDirectory, { recursive: true, force: true });
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -43,7 +43,11 @@ describe('OpenCode Go quota provider', () => {
|
|
|
43
43
|
return new Response(JSON.stringify({ usage: { rolling: { percent: 25, resetsAt: '2026-08-12T12:00:00.000Z' } } }));
|
|
44
44
|
});
|
|
45
45
|
expect(request.url).toBe('https://opencode.ai/zen/go/v1/usage');
|
|
46
|
-
expect(request.options.headers).toMatchObject({
|
|
46
|
+
expect(request.options.headers).toMatchObject({
|
|
47
|
+
Accept: 'application/json',
|
|
48
|
+
Authorization: 'Bearer secret',
|
|
49
|
+
'x-opencode-session': 'openchamber-usage',
|
|
50
|
+
});
|
|
47
51
|
expect(request.options.headers.Cookie).toBeUndefined();
|
|
48
52
|
expect(usage['5h'].usedPercent).toBe(25);
|
|
49
53
|
});
|
|
@@ -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 = [
|
|
@@ -41,7 +41,7 @@ Relay is not a separate link format: it is one transport candidate inside the un
|
|
|
41
41
|
Everything a client normally sends to the single AiWorker origin:
|
|
42
42
|
- **HTTP** — REST endpoints and proxied OpenCode SDK calls under `/api/*`, plus `/auth/*` and `/health`.
|
|
43
43
|
- **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).
|
|
44
|
+
- **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
45
|
|
|
46
46
|
The host dispatcher restricts tunneled traffic to explicit path allowlists (one for HTTP, one for WS).
|
|
47
47
|
|
|
@@ -33,7 +33,7 @@ const DEFAULT_BATCH_WINDOW_MS = 150;
|
|
|
33
33
|
// the 150 ms default. Only applies on directions where batching was negotiated.
|
|
34
34
|
const resolveBatchWindowMs = (option) => {
|
|
35
35
|
if (Number.isFinite(option) && option >= 0) return option;
|
|
36
|
-
const envValue = Number.parseInt(process.env.
|
|
36
|
+
const envValue = Number.parseInt(process.env.AIWORKER_RELAY_BATCH_WINDOW_MS ?? '', 10);
|
|
37
37
|
if (Number.isFinite(envValue) && envValue >= 0) return envValue;
|
|
38
38
|
return DEFAULT_BATCH_WINDOW_MS;
|
|
39
39
|
};
|
|
@@ -98,6 +98,9 @@ const startFakeRelay = () => {
|
|
|
98
98
|
wsUrl: `ws://127.0.0.1:${port}`,
|
|
99
99
|
state,
|
|
100
100
|
stop: () => new Promise((r) => {
|
|
101
|
+
// A socket a failed test left open would hold server.close() until
|
|
102
|
+
// the hook timeout; drop them so a failure is reported once.
|
|
103
|
+
for (const client of wss.clients) client.terminate();
|
|
101
104
|
wss.close();
|
|
102
105
|
server.close(() => r());
|
|
103
106
|
}),
|
|
@@ -162,7 +165,6 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
|
|
|
162
165
|
url.searchParams.set('role', 'client');
|
|
163
166
|
url.searchParams.set('serverId', serverId);
|
|
164
167
|
url.searchParams.set('connectionId', connectionId);
|
|
165
|
-
const ws = new WebSocket(url.toString());
|
|
166
168
|
|
|
167
169
|
const hostPub = await globalThis.crypto.subtle.importKey(
|
|
168
170
|
'jwk',
|
|
@@ -182,6 +184,12 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
|
|
|
182
184
|
resolveDone = resolve;
|
|
183
185
|
});
|
|
184
186
|
|
|
187
|
+
// Dialed only now, with the key material ready and the listeners attached
|
|
188
|
+
// in the same tick. Dialing before the WebCrypto awaits above let a loopback
|
|
189
|
+
// socket open while the key generation was still queued on the threadpool,
|
|
190
|
+
// and an `open` event with no listener means no hello, no ready, and a
|
|
191
|
+
// client that waits forever. Loaded CI runners hit exactly that.
|
|
192
|
+
const ws = new WebSocket(url.toString());
|
|
185
193
|
ws.on('open', async () => {
|
|
186
194
|
ws.send(JSON.stringify({
|
|
187
195
|
t: 'hello',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Per-machine relay-host claim. Every
|
|
1
|
+
// Per-machine relay-host claim. Every AiWorker instance on a machine shares
|
|
2
2
|
// the same data dir and therefore the same relay signing key / serverId, so if
|
|
3
3
|
// two processes run a relay host at once they fight over the single host slot
|
|
4
4
|
// at the relay worker (each new connection closes the previous one with
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Config lives in the server settings file as `settings.privateRelay =
|
|
5
5
|
// { enabled, relayUrl }` (same storage precedent as tunnels/notifications).
|
|
6
|
-
// Routes are registered with the other
|
|
6
|
+
// Routes are registered with the other AiWorker feature routes, before the
|
|
7
7
|
// generic OpenCode proxy, and are covered by the same global UI auth gate.
|
|
8
8
|
//
|
|
9
9
|
// Cross-runtime parity note: relay host mode intentionally targets the web
|
|
@@ -16,7 +16,7 @@ import express from 'express';
|
|
|
16
16
|
import { createRelayIdentityRuntime } from './identity.js';
|
|
17
17
|
import { startRelayHost } from './host-client.js';
|
|
18
18
|
|
|
19
|
-
export const DEFAULT_RELAY_URL = 'wss://relay.
|
|
19
|
+
export const DEFAULT_RELAY_URL = 'wss://relay.aiworker.dev/ws';
|
|
20
20
|
|
|
21
21
|
const isValidRelayUrl = (value) => {
|
|
22
22
|
if (typeof value !== 'string') return false;
|
|
@@ -40,7 +40,7 @@ const normalizeRelayUrl = (value) => {
|
|
|
40
40
|
// stored setting entirely, so the host connection, the pairing offer, and the
|
|
41
41
|
// status all point at it — clients then inherit it from the offer automatically.
|
|
42
42
|
const envRelayUrlOverride = () => {
|
|
43
|
-
const raw = process.env.
|
|
43
|
+
const raw = process.env.AIWORKER_RELAY_URL;
|
|
44
44
|
if (typeof raw !== 'string' || !raw.trim() || !isValidRelayUrl(raw)) return null;
|
|
45
45
|
return raw.trim();
|
|
46
46
|
};
|
|
@@ -100,7 +100,7 @@ export const createRelayService = ({
|
|
|
100
100
|
return {
|
|
101
101
|
enabled: stored?.enabled === true,
|
|
102
102
|
relayUrl: override ?? normalizeRelayUrl(stored?.relayUrl),
|
|
103
|
-
// True when the endpoint is pinned by
|
|
103
|
+
// True when the endpoint is pinned by AIWORKER_RELAY_URL (a self-hosted
|
|
104
104
|
// relay); the stored setting is ignored while it is set.
|
|
105
105
|
relayUrlLocked: override !== null,
|
|
106
106
|
};
|
|
@@ -122,7 +122,7 @@ export const createRelayService = ({
|
|
|
122
122
|
|
|
123
123
|
const standbyStatus = (holderPid) => ({
|
|
124
124
|
state: 'standby',
|
|
125
|
-
lastError: `relay host is owned by another local
|
|
125
|
+
lastError: `relay host is owned by another local AiWorker process (pid ${holderPid})`,
|
|
126
126
|
connectedClients: 0,
|
|
127
127
|
});
|
|
128
128
|
|
|
@@ -33,7 +33,9 @@ const ALLOWED_WS_PATHS = new Set([
|
|
|
33
33
|
'/api/event/ws',
|
|
34
34
|
'/api/terminal/ws',
|
|
35
35
|
'/api/dictation/ws',
|
|
36
|
+
'/api/dev-tunnel',
|
|
36
37
|
]);
|
|
38
|
+
export const isAllowedRelayWebSocketPath = (pathname) => ALLOWED_WS_PATHS.has(pathname);
|
|
37
39
|
|
|
38
40
|
// Hop-by-hop headers stripped from tunneled requests; `host` is set by fetch
|
|
39
41
|
// to the loopback origin. content-length is dropped too because the body is
|
|
@@ -425,7 +427,7 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
425
427
|
void sendAbort(streamId, error?.message ?? 'malformed ws open');
|
|
426
428
|
return;
|
|
427
429
|
}
|
|
428
|
-
if (!
|
|
430
|
+
if (!isAllowedRelayWebSocketPath(open.path)) {
|
|
429
431
|
void sendAbort(streamId, 'Path is not allowed through the relay');
|
|
430
432
|
return;
|
|
431
433
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
|
|
4
|
-
import { createTunnelHost } from './tunnel-host.js';
|
|
4
|
+
import { createTunnelHost, isAllowedRelayWebSocketPath } from './tunnel-host.js';
|
|
5
5
|
import { decodeTunnelFrame, encodeTunnelFrame, encodeJsonPayload, TunnelFrameType } from './tunnel-codec.js';
|
|
6
6
|
|
|
7
7
|
const startLoopback = () =>
|
|
@@ -145,3 +145,11 @@ describe('tunnel-host HTTP body forwarding', () => {
|
|
|
145
145
|
await loopback.stop();
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe('relay host WebSocket allowlist', () => {
|
|
150
|
+
test('allows only the exact dev-server tunnel path', () => {
|
|
151
|
+
expect(isAllowedRelayWebSocketPath('/api/dev-tunnel')).toBe(true);
|
|
152
|
+
expect(isAllowedRelayWebSocketPath('/api/dev-tunnel/')).toBe(false);
|
|
153
|
+
expect(isAllowedRelayWebSocketPath('/api/database/ws')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Regression for https://
|
|
4
|
+
* Regression for https://roweb.cn/roweb/openchamber/issues/2710
|
|
5
5
|
* "Scheduled daily task executes twice at the configured time"
|
|
6
6
|
*
|
|
7
|
-
* Root cause: each
|
|
7
|
+
* Root cause: each AiWorker server process keeps its own timers. Two
|
|
8
8
|
* instances that share the same on-disk project config (CLI serve on port 3000
|
|
9
9
|
* + Electron on 57123, or a startup login service + desktop) each arm a timer
|
|
10
10
|
* for the same occurrence and both dispatch.
|