@iislee/opencodex 2.11.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/LICENSE +21 -0
- package/README.md +225 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +471 -0
- package/src/service.ts +2554 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { probeHostname } from "../proxy-liveness";
|
|
3
|
+
|
|
4
|
+
export interface ApiAccessEndpoints {
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
responsesEndpoint: string;
|
|
7
|
+
chatCompletionsEndpoint: string;
|
|
8
|
+
messagesEndpoint: string;
|
|
9
|
+
modelsEndpoint: string;
|
|
10
|
+
claudeCodeEnabled: boolean;
|
|
11
|
+
/** Back-compat alias for older GUI clients. */
|
|
12
|
+
endpoint: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BuildApiAccessEndpointsOptions = {
|
|
16
|
+
/** Full request URL (preferred when the bind host is a wildcard). */
|
|
17
|
+
requestUrl?: string | URL | null;
|
|
18
|
+
/** Raw `Host` header from the inbound request. */
|
|
19
|
+
requestHost?: string | null;
|
|
20
|
+
/** Raw `Origin` header from the inbound request. */
|
|
21
|
+
requestOrigin?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function isWildcardBindHost(hostname: string | undefined): boolean {
|
|
25
|
+
const trimmed = (hostname ?? "").trim();
|
|
26
|
+
return !trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Bracket bare IPv6 literals for URL authority composition. */
|
|
30
|
+
export function formatAuthorityHost(hostname: string): string {
|
|
31
|
+
const trimmed = hostname.trim();
|
|
32
|
+
if (!trimmed) return "127.0.0.1";
|
|
33
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
34
|
+
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hostnameFromAuthority(authority: string): string | null {
|
|
38
|
+
const raw = authority.trim();
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
if (raw.startsWith("[")) {
|
|
41
|
+
const end = raw.indexOf("]");
|
|
42
|
+
if (end > 0) return raw.slice(1, end) || null;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Host headers for IPv6 are bracketed; treat multi-colon values as unusable.
|
|
46
|
+
if (raw.includes(":") && raw.split(":").length > 2) return null;
|
|
47
|
+
const colon = raw.lastIndexOf(":");
|
|
48
|
+
if (colon > 0 && /^\d+$/.test(raw.slice(colon + 1))) {
|
|
49
|
+
return raw.slice(0, colon) || null;
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function originBaseUrl(raw: string): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(raw);
|
|
57
|
+
if (!url.hostname || isWildcardBindHost(url.hostname)) return null;
|
|
58
|
+
return `${url.protocol}//${url.host}/v1`;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prefer the client's request host/origin when the proxy is bound to a wildcard.
|
|
66
|
+
* Falls back to loopback only when no usable request context is available.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveApiAccessBaseUrl(
|
|
69
|
+
config: Pick<OcxConfig, "hostname" | "port">,
|
|
70
|
+
opts: BuildApiAccessEndpointsOptions = {},
|
|
71
|
+
): string {
|
|
72
|
+
const port = config.port ?? 10100;
|
|
73
|
+
|
|
74
|
+
if (!isWildcardBindHost(config.hostname)) {
|
|
75
|
+
return `http://${probeHostname(config.hostname)}:${port}/v1`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromOrigin = opts.requestOrigin ? originBaseUrl(opts.requestOrigin) : null;
|
|
79
|
+
if (fromOrigin) return fromOrigin;
|
|
80
|
+
|
|
81
|
+
if (opts.requestUrl) {
|
|
82
|
+
try {
|
|
83
|
+
const url = typeof opts.requestUrl === "string" ? new URL(opts.requestUrl) : opts.requestUrl;
|
|
84
|
+
if (url.hostname && !isWildcardBindHost(url.hostname)) {
|
|
85
|
+
return `${url.protocol}//${url.host}/v1`;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
/* ignore malformed request URL */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hostHeader = opts.requestHost?.trim();
|
|
93
|
+
if (hostHeader) {
|
|
94
|
+
const hostname = hostnameFromAuthority(hostHeader);
|
|
95
|
+
if (hostname && !isWildcardBindHost(hostname)) {
|
|
96
|
+
// Preserve an explicit port from the Host header; otherwise use the bind port.
|
|
97
|
+
const hasPort = hostHeader.startsWith("[")
|
|
98
|
+
? /\]:\d+$/.test(hostHeader)
|
|
99
|
+
: /:\d+$/.test(hostHeader) && hostHeader.split(":").length === 2;
|
|
100
|
+
const authority = hasPort
|
|
101
|
+
? (hostHeader.startsWith("[") ? hostHeader : hostHeader)
|
|
102
|
+
: `${formatAuthorityHost(hostname)}:${port}`;
|
|
103
|
+
return `http://${authority}/v1`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `http://127.0.0.1:${port}/v1`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @deprecated Prefer resolveApiAccessBaseUrl; retained for focused host-format tests. */
|
|
111
|
+
export function resolveApiAccessDisplayHost(
|
|
112
|
+
configHostname: string | undefined,
|
|
113
|
+
opts: BuildApiAccessEndpointsOptions = {},
|
|
114
|
+
): string {
|
|
115
|
+
if (!isWildcardBindHost(configHostname)) {
|
|
116
|
+
return probeHostname(configHostname);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return new URL(resolveApiAccessBaseUrl({ hostname: configHostname, port: 10100 }, opts)).hostname
|
|
120
|
+
|| "127.0.0.1";
|
|
121
|
+
} catch {
|
|
122
|
+
return "127.0.0.1";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildApiAccessEndpoints(
|
|
127
|
+
config: OcxConfig,
|
|
128
|
+
opts: BuildApiAccessEndpointsOptions = {},
|
|
129
|
+
): ApiAccessEndpoints {
|
|
130
|
+
const baseUrl = resolveApiAccessBaseUrl(config, opts);
|
|
131
|
+
const responsesEndpoint = `${baseUrl}/responses`;
|
|
132
|
+
return {
|
|
133
|
+
baseUrl,
|
|
134
|
+
responsesEndpoint,
|
|
135
|
+
chatCompletionsEndpoint: `${baseUrl}/chat/completions`,
|
|
136
|
+
messagesEndpoint: `${baseUrl}/messages`,
|
|
137
|
+
modelsEndpoint: `${baseUrl}/models`,
|
|
138
|
+
claudeCodeEnabled: config.claudeCode?.enabled !== false,
|
|
139
|
+
endpoint: responsesEndpoint,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentUsageLogRevision,
|
|
3
|
+
readUsageSnapshotForManagement,
|
|
4
|
+
usageLogRevisionKey,
|
|
5
|
+
type PersistedUsageEntry,
|
|
6
|
+
} from "../../usage/log";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-key usage as the API tab renders it.
|
|
10
|
+
*
|
|
11
|
+
* A discriminated union rather than numbers with a flag beside them: when two
|
|
12
|
+
* config entries share an id there IS no per-key total, and an optional marker
|
|
13
|
+
* next to `requests7d: 7` invites a consumer to render the 7 anyway.
|
|
14
|
+
*/
|
|
15
|
+
export type ApiKeyUsage =
|
|
16
|
+
| { ambiguous: true }
|
|
17
|
+
| { ambiguous?: false; requests7d: number; totalRequests: number; lastUsedAt?: string };
|
|
18
|
+
|
|
19
|
+
export interface ApiKeyUsageSnapshot {
|
|
20
|
+
rollup: Map<string, ApiKeyUsage>;
|
|
21
|
+
historyTruncated?: true;
|
|
22
|
+
/**
|
|
23
|
+
* Earliest row carrying a recognized `admissionKind`. A property of the DATA
|
|
24
|
+
* SET, not of a key, so it is singular and lives beside the map: it is what
|
|
25
|
+
* lets the GUI tell "this key was used zero times" from "nothing is
|
|
26
|
+
* attributable yet". Keyed on the kind rather than on `apiKeyId`, because an
|
|
27
|
+
* environment or loopback row is attributed traffic with no configured key.
|
|
28
|
+
*/
|
|
29
|
+
attributionSince?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A timestamp we can actually do date arithmetic with.
|
|
36
|
+
*
|
|
37
|
+
* `usage.jsonl` is hand-editable and JSON permits numbers outside the Date
|
|
38
|
+
* range: `1e309` survives normalization and then throws `RangeError` from
|
|
39
|
+
* `toISOString()`. Since the caller catches to protect key management, one bad
|
|
40
|
+
* row would have zeroed the rollup for EVERY key — active keys reported as
|
|
41
|
+
* unused is exactly the wrong answer to hand someone deciding what to delete.
|
|
42
|
+
*/
|
|
43
|
+
function usableTimestamp(value: unknown): number | null {
|
|
44
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
45
|
+
return Number.isNaN(new Date(value).getTime()) ? null : value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pure: one pass over an already-read snapshot, so it is unit-testable without
|
|
50
|
+
* touching the filesystem.
|
|
51
|
+
*
|
|
52
|
+
* Rows are bucketed only when `admissionKind === "configured"`. Keying on
|
|
53
|
+
* `apiKeyId` alone would let a hand-edited entry whose id is `loopback` absorb
|
|
54
|
+
* traffic it never admitted.
|
|
55
|
+
*/
|
|
56
|
+
export function rollupApiKeyUsage(
|
|
57
|
+
entries: PersistedUsageEntry[],
|
|
58
|
+
configuredIds: string[],
|
|
59
|
+
now: number = Date.now(),
|
|
60
|
+
): ApiKeyUsageSnapshot {
|
|
61
|
+
const duplicated = new Set<string>();
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
for (const id of configuredIds) {
|
|
64
|
+
if (seen.has(id)) duplicated.add(id);
|
|
65
|
+
seen.add(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const totals = new Map<string, { requests7d: number; totalRequests: number; lastUsedAt?: string }>();
|
|
69
|
+
let attributionSince: number | undefined;
|
|
70
|
+
const cutoff = now - SEVEN_DAYS_MS;
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.admissionKind) continue;
|
|
74
|
+
const timestamp = usableTimestamp(entry.timestamp);
|
|
75
|
+
if (timestamp !== null && (attributionSince === undefined || timestamp < attributionSince)) {
|
|
76
|
+
attributionSince = timestamp;
|
|
77
|
+
}
|
|
78
|
+
if (entry.admissionKind !== "configured" || !entry.apiKeyId) continue;
|
|
79
|
+
|
|
80
|
+
const bucket = totals.get(entry.apiKeyId) ?? { requests7d: 0, totalRequests: 0 };
|
|
81
|
+
// The request happened even if its clock reading is unusable, so it still
|
|
82
|
+
// counts toward the total; only the time-based fields are skipped.
|
|
83
|
+
bucket.totalRequests += 1;
|
|
84
|
+
if (timestamp !== null) {
|
|
85
|
+
if (timestamp >= cutoff) bucket.requests7d += 1;
|
|
86
|
+
const iso = new Date(timestamp).toISOString();
|
|
87
|
+
if (!bucket.lastUsedAt || iso > bucket.lastUsedAt) bucket.lastUsedAt = iso;
|
|
88
|
+
}
|
|
89
|
+
totals.set(entry.apiKeyId, bucket);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rollup = new Map<string, ApiKeyUsage>();
|
|
93
|
+
for (const id of configuredIds) {
|
|
94
|
+
if (duplicated.has(id)) {
|
|
95
|
+
rollup.set(id, { ambiguous: true });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
rollup.set(id, totals.get(id) ?? { requests7d: 0, totalRequests: 0 });
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
rollup,
|
|
102
|
+
...(attributionSince !== undefined ? { attributionSince: new Date(attributionSince).toISOString() } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Rollup cache keyed by the exact usage-log revision, mirroring the /api/usage
|
|
108
|
+
* summary cache. Without it, every key-list read reparses an append-only log
|
|
109
|
+
* that only ever grows — and the GUI fetches this route on mount and after every
|
|
110
|
+
* create/rename/delete. The compact rollup is a handful of counters per key, so
|
|
111
|
+
* caching it costs nothing; a new row changes the revision and invalidates it.
|
|
112
|
+
*/
|
|
113
|
+
let rollupCache: { revisionKey: string; expiresAt: number; snapshot: ApiKeyUsageSnapshot } | null = null;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The rollup is a function of the log AND of the clock: a request ages out of
|
|
117
|
+
* the seven-day window with no write to bump the file revision, so a purely
|
|
118
|
+
* revision-keyed entry would report a stale `requests7d` indefinitely.
|
|
119
|
+
*
|
|
120
|
+
* A minute is the whole rule. Deriving the exact next-transition instant would
|
|
121
|
+
* mean tracking the OLDEST counted request per key, which the compact rollup
|
|
122
|
+
* deliberately does not keep — and a count that can be at most 60s stale is
|
|
123
|
+
* already far tighter than the window it describes.
|
|
124
|
+
*/
|
|
125
|
+
const ROLLUP_CACHE_TTL_MS = 60_000;
|
|
126
|
+
|
|
127
|
+
/** Test seam: the cache is module state and would otherwise leak between cases. */
|
|
128
|
+
export function clearApiKeyUsageCacheForTests(): void {
|
|
129
|
+
rollupCache = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reads the durable usage snapshot the way /api/usage does, then rolls it up.
|
|
134
|
+
*
|
|
135
|
+
* Never throws: an unreadable snapshot yields empty rollups and no
|
|
136
|
+
* `attributionSince`. Key management working matters more than usage numbers
|
|
137
|
+
* being present, and the GUI already treats an absent field as "no data".
|
|
138
|
+
*/
|
|
139
|
+
export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
|
|
140
|
+
// JSON rather than a joined string: ids are only validated as non-empty
|
|
141
|
+
// strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
|
|
142
|
+
// config could be served the other's cached rollup.
|
|
143
|
+
const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
try {
|
|
146
|
+
const observedKey = `${usageLogRevisionKey(currentUsageLogRevision())}|${idsKey}`;
|
|
147
|
+
if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt) {
|
|
148
|
+
return rollupCache.snapshot;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const snapshot = await readUsageSnapshotForManagement(maxReadBytes);
|
|
152
|
+
const rolled = {
|
|
153
|
+
...rollupApiKeyUsage(snapshot.entries, configuredIds, now),
|
|
154
|
+
...(snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated ? { historyTruncated: true as const } : {}),
|
|
155
|
+
};
|
|
156
|
+
rollupCache = {
|
|
157
|
+
revisionKey: `${usageLogRevisionKey(snapshot.revision)}|${idsKey}`,
|
|
158
|
+
expiresAt: now + ROLLUP_CACHE_TTL_MS,
|
|
159
|
+
snapshot: rolled,
|
|
160
|
+
};
|
|
161
|
+
return rolled;
|
|
162
|
+
} catch {
|
|
163
|
+
const rollup = new Map<string, ApiKeyUsage>();
|
|
164
|
+
for (const id of configuredIds) rollup.set(id, { requests7d: 0, totalRequests: 0 });
|
|
165
|
+
return { rollup };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { jsonResponse } from "../auth-cors";
|
|
3
|
+
import {
|
|
4
|
+
DecompressedBodyTooLargeError,
|
|
5
|
+
readBoundedJsonRequestBody,
|
|
6
|
+
} from "../request-decompress";
|
|
7
|
+
|
|
8
|
+
export const MANAGEMENT_JSON_BODY_MAX_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export function readManagementJsonBody<T = unknown>(req: Request): Promise<T> {
|
|
11
|
+
return readBoundedJsonRequestBody(req, MANAGEMENT_JSON_BODY_MAX_BYTES) as Promise<T>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function managementBodyTooLargeResponse(
|
|
15
|
+
error: unknown,
|
|
16
|
+
req: Request,
|
|
17
|
+
config: OcxConfig,
|
|
18
|
+
): Response | null {
|
|
19
|
+
return error instanceof DecompressedBodyTooLargeError
|
|
20
|
+
? jsonResponse({ error: "request body too large" }, 413, req, config)
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function rethrowManagementBodyTooLarge(error: unknown): void {
|
|
25
|
+
if (error instanceof DecompressedBodyTooLargeError) throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readManagementJsonBodyOr<T>(req: Request, fallback: T): Promise<unknown | T> {
|
|
29
|
+
try {
|
|
30
|
+
return await readManagementJsonBody(req);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
rethrowManagementBodyTooLarge(error);
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clients effective-status management route.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/clients/status — read-only snapshot of each coding agent's on-disk
|
|
5
|
+
* routing (base URL / model / verdict). Never serializes secrets.
|
|
6
|
+
*/
|
|
7
|
+
import { readRuntimePort } from "../../config";
|
|
8
|
+
import { jsonResponse } from "../auth-cors";
|
|
9
|
+
import type { ManagementContext } from "./context";
|
|
10
|
+
|
|
11
|
+
export async function handleClientsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
12
|
+
const { req, url, config } = ctx;
|
|
13
|
+
|
|
14
|
+
if (url.pathname === "/api/clients/status" && req.method === "GET") {
|
|
15
|
+
try {
|
|
16
|
+
const { readClientsEffectiveStatus } = await import("../../clients/effective-status");
|
|
17
|
+
const { findLiveProxy } = await import("../proxy-liveness");
|
|
18
|
+
|
|
19
|
+
// Prefer live bind metadata so the page compares client base URLs against the
|
|
20
|
+
// port the proxy actually answered on, not just config.port.
|
|
21
|
+
let livePort: number | undefined;
|
|
22
|
+
let liveHostname: string | undefined;
|
|
23
|
+
let running = false;
|
|
24
|
+
try {
|
|
25
|
+
const live = await findLiveProxy();
|
|
26
|
+
if (live) {
|
|
27
|
+
running = true;
|
|
28
|
+
livePort = live.port;
|
|
29
|
+
liveHostname = live.hostname;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Fall back to runtime-port file / config.
|
|
33
|
+
}
|
|
34
|
+
if (!running) {
|
|
35
|
+
try {
|
|
36
|
+
const runtime = readRuntimePort(process.pid);
|
|
37
|
+
if (runtime?.port) {
|
|
38
|
+
// Runtime file exists for this process → we are the proxy answering this request.
|
|
39
|
+
running = true;
|
|
40
|
+
livePort = runtime.port;
|
|
41
|
+
liveHostname = runtime.hostname;
|
|
42
|
+
}
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
}
|
|
45
|
+
// Management API is served by the proxy itself, so if we got here the process is up.
|
|
46
|
+
// Still report running=true even when findLiveProxy races during startup.
|
|
47
|
+
if (!running) running = true;
|
|
48
|
+
if (livePort === undefined) livePort = config.port;
|
|
49
|
+
if (liveHostname === undefined) liveHostname = config.hostname;
|
|
50
|
+
|
|
51
|
+
const snapshot = await readClientsEffectiveStatus(config, {
|
|
52
|
+
running,
|
|
53
|
+
livePort,
|
|
54
|
+
liveHostname,
|
|
55
|
+
});
|
|
56
|
+
return jsonResponse(snapshot);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/cloud-sync/* — OneDrive backup/restore for ~/.opencodex (management auth).
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
azureRedirectUri,
|
|
6
|
+
cancelBrowserLogin,
|
|
7
|
+
clearOneDriveToken,
|
|
8
|
+
fetchOneDriveAccountEmail,
|
|
9
|
+
getBrowserLoginStatus,
|
|
10
|
+
pollDeviceCodeTokenOnce,
|
|
11
|
+
readOneDriveToken,
|
|
12
|
+
resolveLoopbackPort,
|
|
13
|
+
resolveMsAuthority,
|
|
14
|
+
startBrowserLoopbackLogin,
|
|
15
|
+
startDeviceCodeLogin,
|
|
16
|
+
writeOneDriveToken,
|
|
17
|
+
} from "../../cloud/onedrive-auth";
|
|
18
|
+
import type { MsAuthority } from "../../cloud/types";
|
|
19
|
+
import { remoteSyncRootDescription } from "../../cloud/onedrive-graph";
|
|
20
|
+
import { pullFromOneDrive, pushToOneDrive, readRemoteManifest } from "../../cloud/sync";
|
|
21
|
+
import {
|
|
22
|
+
getDeviceName,
|
|
23
|
+
getOrCreateDeviceId,
|
|
24
|
+
readCloudSyncSettings,
|
|
25
|
+
resolveClientId,
|
|
26
|
+
writeCloudSyncSettings,
|
|
27
|
+
} from "../../cloud/settings";
|
|
28
|
+
import { jsonResponse } from "../auth-cors";
|
|
29
|
+
import type { ManagementContext } from "./context";
|
|
30
|
+
|
|
31
|
+
interface PendingDeviceLogin {
|
|
32
|
+
clientId: string;
|
|
33
|
+
deviceCode: string;
|
|
34
|
+
userCode: string;
|
|
35
|
+
verificationUri: string;
|
|
36
|
+
verificationUriComplete?: string;
|
|
37
|
+
interval: number;
|
|
38
|
+
expiresAt: number;
|
|
39
|
+
authority: MsAuthority;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let pendingLogin: PendingDeviceLogin | null = null;
|
|
43
|
+
|
|
44
|
+
function statusPayload() {
|
|
45
|
+
const settings = readCloudSyncSettings();
|
|
46
|
+
const token = readOneDriveToken();
|
|
47
|
+
return {
|
|
48
|
+
clientId: settings.clientId ?? process.env.OPENCODEX_MS_CLIENT_ID ?? process.env.OPENCODEX_ONEDRIVE_CLIENT_ID ?? null,
|
|
49
|
+
hasClientSecret: Boolean(settings.clientSecret?.trim() || process.env.OPENCODEX_MS_CLIENT_SECRET?.trim()),
|
|
50
|
+
loggedIn: Boolean(token),
|
|
51
|
+
account: token?.account ?? null,
|
|
52
|
+
msAuthority: resolveMsAuthority(),
|
|
53
|
+
deviceId: getOrCreateDeviceId(),
|
|
54
|
+
deviceName: getDeviceName(),
|
|
55
|
+
lastSyncAt: settings.lastSyncAt ?? null,
|
|
56
|
+
lastSyncDirection: settings.lastSyncDirection ?? null,
|
|
57
|
+
remoteRoot: remoteSyncRootDescription(),
|
|
58
|
+
includeUsage: settings.includeUsage === true,
|
|
59
|
+
includeVault: settings.includeVault !== false,
|
|
60
|
+
loopbackPort: resolveLoopbackPort(),
|
|
61
|
+
/** Exact value to paste into Azure → Mobile and desktop → Redirect URIs */
|
|
62
|
+
azureRedirectUri: azureRedirectUri(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function handleCloudSyncRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
67
|
+
const { req, url, config } = ctx;
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/api/cloud-sync/status" && req.method === "GET") {
|
|
70
|
+
const base = statusPayload();
|
|
71
|
+
let remoteManifest: unknown = null;
|
|
72
|
+
let remoteError: string | null = null;
|
|
73
|
+
if (base.loggedIn) {
|
|
74
|
+
try {
|
|
75
|
+
remoteManifest = await readRemoteManifest();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
remoteError = e instanceof Error ? e.message : String(e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return jsonResponse({ ...base, remoteManifest, remoteError });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (url.pathname === "/api/cloud-sync/client-id" && req.method === "POST") {
|
|
84
|
+
let body: { clientId?: unknown; clientSecret?: unknown };
|
|
85
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON" }, 400); }
|
|
86
|
+
const clientId = typeof body.clientId === "string" ? body.clientId.trim() : "";
|
|
87
|
+
if (!clientId || clientId.length < 8) return jsonResponse({ error: "clientId is required" }, 400);
|
|
88
|
+
const settings = readCloudSyncSettings();
|
|
89
|
+
const next = { ...settings, clientId };
|
|
90
|
+
// Only update secret when the field is present (empty string clears it).
|
|
91
|
+
if ("clientSecret" in body) {
|
|
92
|
+
const secret = typeof body.clientSecret === "string" ? body.clientSecret.trim() : "";
|
|
93
|
+
if (secret) next.clientSecret = secret;
|
|
94
|
+
else delete next.clientSecret;
|
|
95
|
+
}
|
|
96
|
+
writeCloudSyncSettings(next);
|
|
97
|
+
return jsonResponse({
|
|
98
|
+
ok: true,
|
|
99
|
+
clientId,
|
|
100
|
+
hasClientSecret: Boolean(next.clientSecret),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (url.pathname === "/api/cloud-sync/login/start" && req.method === "POST") {
|
|
105
|
+
let body: { clientId?: unknown; msAuthority?: unknown; mode?: unknown } = {};
|
|
106
|
+
try { body = await req.json(); } catch { /* empty ok */ }
|
|
107
|
+
let clientId: string;
|
|
108
|
+
try {
|
|
109
|
+
clientId = resolveClientId(typeof body.clientId === "string" ? body.clientId : undefined);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
112
|
+
}
|
|
113
|
+
const authorityHint = typeof body.msAuthority === "string" ? body.msAuthority : undefined;
|
|
114
|
+
const authority =
|
|
115
|
+
authorityHint === "common" || authorityHint === "consumers" || authorityHint === "organizations"
|
|
116
|
+
? authorityHint
|
|
117
|
+
: undefined;
|
|
118
|
+
const settings = readCloudSyncSettings();
|
|
119
|
+
writeCloudSyncSettings({
|
|
120
|
+
...settings,
|
|
121
|
+
clientId,
|
|
122
|
+
...(authority ? { msAuthority: authority } : {}),
|
|
123
|
+
});
|
|
124
|
+
// Default: browser loopback PKCE (works with Mobile+desktop + http://localhost).
|
|
125
|
+
// mode=device forces classic device-code (needs Allow public client flows / "mobile").
|
|
126
|
+
const mode = body.mode === "device" ? "device" : "browser";
|
|
127
|
+
try {
|
|
128
|
+
if (mode === "browser") {
|
|
129
|
+
cancelBrowserLogin();
|
|
130
|
+
pendingLogin = null;
|
|
131
|
+
const start = await startBrowserLoopbackLogin(clientId, authority);
|
|
132
|
+
return jsonResponse({
|
|
133
|
+
ok: true,
|
|
134
|
+
mode: "browser",
|
|
135
|
+
authUrl: start.authUrl,
|
|
136
|
+
redirectUri: start.redirectUri,
|
|
137
|
+
azureRedirectUri: start.azureRedirectUri,
|
|
138
|
+
verificationUri: start.authUrl,
|
|
139
|
+
verificationUriComplete: start.authUrl,
|
|
140
|
+
message:
|
|
141
|
+
`Complete sign-in in the browser. Azure must list redirect URI exactly: ${start.azureRedirectUri}`,
|
|
142
|
+
expiresIn: start.expiresIn,
|
|
143
|
+
interval: 2,
|
|
144
|
+
msAuthority: start.authority,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
cancelBrowserLogin();
|
|
148
|
+
const start = await startDeviceCodeLogin(clientId, authority);
|
|
149
|
+
pendingLogin = {
|
|
150
|
+
clientId,
|
|
151
|
+
deviceCode: start.deviceCode,
|
|
152
|
+
userCode: start.userCode,
|
|
153
|
+
verificationUri: start.verificationUri,
|
|
154
|
+
verificationUriComplete: start.verificationUriComplete,
|
|
155
|
+
interval: start.interval,
|
|
156
|
+
expiresAt: Date.now() + start.expiresIn * 1000,
|
|
157
|
+
authority: start.authority,
|
|
158
|
+
};
|
|
159
|
+
return jsonResponse({
|
|
160
|
+
ok: true,
|
|
161
|
+
mode: "device",
|
|
162
|
+
userCode: start.userCode,
|
|
163
|
+
verificationUri: start.verificationUri,
|
|
164
|
+
verificationUriComplete: start.verificationUriComplete ?? null,
|
|
165
|
+
message: start.message,
|
|
166
|
+
expiresIn: start.expiresIn,
|
|
167
|
+
interval: start.interval,
|
|
168
|
+
msAuthority: start.authority,
|
|
169
|
+
});
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (url.pathname === "/api/cloud-sync/login/poll" && req.method === "POST") {
|
|
176
|
+
// Browser loopback session takes precedence when active.
|
|
177
|
+
const browser = getBrowserLoginStatus();
|
|
178
|
+
if (browser.status === "pending") {
|
|
179
|
+
return jsonResponse({ ok: true, pending: true, mode: "browser" });
|
|
180
|
+
}
|
|
181
|
+
if (browser.status === "ok") {
|
|
182
|
+
const account = browser.account ?? browser.store.account ?? null;
|
|
183
|
+
cancelBrowserLogin();
|
|
184
|
+
return jsonResponse({ ok: true, pending: false, mode: "browser", account, msAuthority: browser.store.authority });
|
|
185
|
+
}
|
|
186
|
+
if (browser.status === "error") {
|
|
187
|
+
const err = browser.error;
|
|
188
|
+
cancelBrowserLogin();
|
|
189
|
+
return jsonResponse({ pending: false, mode: "browser", error: err }, 400);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!pendingLogin) return jsonResponse({ pending: false, error: "no login in progress" }, 400);
|
|
193
|
+
if (Date.now() > pendingLogin.expiresAt) {
|
|
194
|
+
pendingLogin = null;
|
|
195
|
+
return jsonResponse({ pending: false, error: "device code expired" }, 400);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const result = await pollDeviceCodeTokenOnce(
|
|
199
|
+
pendingLogin.clientId,
|
|
200
|
+
pendingLogin.deviceCode,
|
|
201
|
+
pendingLogin.authority,
|
|
202
|
+
);
|
|
203
|
+
if (result.status === "pending") {
|
|
204
|
+
return jsonResponse({ ok: true, pending: true, mode: "device", error: result.error });
|
|
205
|
+
}
|
|
206
|
+
if (result.status === "error") {
|
|
207
|
+
pendingLogin = null;
|
|
208
|
+
return jsonResponse({ pending: false, mode: "device", error: result.error }, 400);
|
|
209
|
+
}
|
|
210
|
+
const store = result.store;
|
|
211
|
+
const email = await fetchOneDriveAccountEmail(store.accessToken);
|
|
212
|
+
if (email) {
|
|
213
|
+
store.account = email;
|
|
214
|
+
writeOneDriveToken(store);
|
|
215
|
+
}
|
|
216
|
+
pendingLogin = null;
|
|
217
|
+
return jsonResponse({ ok: true, pending: false, mode: "device", account: email ?? null, msAuthority: store.authority });
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return jsonResponse({ pending: false, error: e instanceof Error ? e.message : String(e) }, 500);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (url.pathname === "/api/cloud-sync/logout" && req.method === "POST") {
|
|
224
|
+
pendingLogin = null;
|
|
225
|
+
cancelBrowserLogin();
|
|
226
|
+
clearOneDriveToken();
|
|
227
|
+
return jsonResponse({ ok: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (url.pathname === "/api/cloud-sync/push" && req.method === "POST") {
|
|
231
|
+
let body: { passphrase?: unknown; includeUsage?: unknown; noVault?: unknown } = {};
|
|
232
|
+
try { body = await req.json(); } catch { /* empty ok */ }
|
|
233
|
+
const passphrase = typeof body.passphrase === "string" ? body.passphrase : undefined;
|
|
234
|
+
const includeUsage = body.includeUsage === true;
|
|
235
|
+
const noVault = body.noVault === true;
|
|
236
|
+
try {
|
|
237
|
+
resolveClientId();
|
|
238
|
+
const result = await pushToOneDrive({
|
|
239
|
+
passphrase: noVault ? undefined : passphrase,
|
|
240
|
+
includeUsage,
|
|
241
|
+
includeVault: !noVault && Boolean(passphrase),
|
|
242
|
+
});
|
|
243
|
+
return jsonResponse({ ok: true, ...result });
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (url.pathname === "/api/cloud-sync/pull" && req.method === "POST") {
|
|
250
|
+
let body: { passphrase?: unknown; includeUsage?: unknown; yes?: unknown } = {};
|
|
251
|
+
try { body = await req.json(); } catch { /* empty ok */ }
|
|
252
|
+
if (body.yes !== true) return jsonResponse({ error: "pull requires yes: true (overwrites local state)" }, 400);
|
|
253
|
+
const passphrase = typeof body.passphrase === "string" ? body.passphrase : undefined;
|
|
254
|
+
const includeUsage = body.includeUsage === true;
|
|
255
|
+
try {
|
|
256
|
+
resolveClientId();
|
|
257
|
+
const result = await pullFromOneDrive({ yes: true, passphrase, includeUsage });
|
|
258
|
+
return jsonResponse({ ok: true, ...result });
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
void config;
|
|
265
|
+
return null;
|
|
266
|
+
}
|