@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,517 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
ConfigMutationLockError,
|
|
6
|
+
getConfigDir,
|
|
7
|
+
atomicWriteFile,
|
|
8
|
+
backupInvalidConfig,
|
|
9
|
+
hardenConfigDir,
|
|
10
|
+
hardenExistingSecret,
|
|
11
|
+
withConfigMutationLockSync,
|
|
12
|
+
} from "../config";
|
|
13
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
14
|
+
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
15
|
+
|
|
16
|
+
type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
|
|
17
|
+
type CodexAccountStore = Record<string, CodexAccountCredentialRecord>;
|
|
18
|
+
type RawCodexAccountStore = Record<string, CodexAccountCredentials | CodexAccountCredentialRecord>;
|
|
19
|
+
|
|
20
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
21
|
+
const REFRESH_LOCK_STALE_MS = 60_000;
|
|
22
|
+
const REFRESH_LOCK_WAIT_MS = REFRESH_LOCK_STALE_MS + 5_000;
|
|
23
|
+
const REFRESH_LOCK_POLL_MS = 50;
|
|
24
|
+
|
|
25
|
+
function codexAccountsPath(): string {
|
|
26
|
+
return join(getConfigDir(), "codex-accounts.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadCodexAccountStore(): LegacyCodexAccountStore {
|
|
30
|
+
const records = loadCodexAccountRecordStore();
|
|
31
|
+
const credentials: LegacyCodexAccountStore = {};
|
|
32
|
+
for (const [id, record] of Object.entries(records)) {
|
|
33
|
+
if (record.deletedAt == null && record.credential) credentials[id] = record.credential;
|
|
34
|
+
}
|
|
35
|
+
return credentials;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isCredential(value: unknown): value is CodexAccountCredentials {
|
|
43
|
+
return isObject(value)
|
|
44
|
+
&& typeof value.accessToken === "string"
|
|
45
|
+
&& typeof value.refreshToken === "string"
|
|
46
|
+
&& typeof value.expiresAt === "number"
|
|
47
|
+
&& typeof value.chatgptAccountId === "string";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isCredentialRecord(value: unknown): value is CodexAccountCredentialRecord {
|
|
51
|
+
return isObject(value)
|
|
52
|
+
&& typeof value.generation === "number"
|
|
53
|
+
&& (value.credential === undefined || isCredential(value.credential))
|
|
54
|
+
&& (value.refreshGrantFingerprint === undefined || typeof value.refreshGrantFingerprint === "string")
|
|
55
|
+
&& (value.deletedAt === undefined || typeof value.deletedAt === "number")
|
|
56
|
+
&& (value.replacedAt === undefined || typeof value.replacedAt === "number")
|
|
57
|
+
&& (value.lastCodexValidatedAt === undefined || typeof value.lastCodexValidatedAt === "number")
|
|
58
|
+
&& (value.lastCodexValidationStatus === undefined || value.lastCodexValidationStatus === "ok" || value.lastCodexValidationStatus === "failed")
|
|
59
|
+
&& (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function refreshGrantFingerprintForToken(refreshToken: string): string {
|
|
63
|
+
return createHash("sha256").update(`codex-refresh-grant:${refreshToken}`).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function recordGrantFingerprint(record: CodexAccountCredentialRecord): string | undefined {
|
|
67
|
+
return record.refreshGrantFingerprint ?? (
|
|
68
|
+
record.credential ? refreshGrantFingerprintForToken(record.credential.refreshToken) : undefined
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeRecord(value: CodexAccountCredentials | CodexAccountCredentialRecord | undefined): CodexAccountCredentialRecord | undefined {
|
|
73
|
+
if (!value) return undefined;
|
|
74
|
+
if (isCredentialRecord(value)) {
|
|
75
|
+
const refreshGrantFingerprint = recordGrantFingerprint(value);
|
|
76
|
+
return refreshGrantFingerprint ? { ...value, refreshGrantFingerprint } : value;
|
|
77
|
+
}
|
|
78
|
+
if (isCredential(value)) {
|
|
79
|
+
return {
|
|
80
|
+
credential: value,
|
|
81
|
+
generation: 0,
|
|
82
|
+
refreshGrantFingerprint: refreshGrantFingerprintForToken(value.refreshToken),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function loadCodexAccountRecordStore(): CodexAccountStore {
|
|
89
|
+
const path = codexAccountsPath();
|
|
90
|
+
hardenConfigDir();
|
|
91
|
+
hardenExistingSecret(path);
|
|
92
|
+
if (!existsSync(path)) return {};
|
|
93
|
+
try {
|
|
94
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
|
|
95
|
+
const normalized: CodexAccountStore = {};
|
|
96
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
97
|
+
const record = normalizeRecord(value);
|
|
98
|
+
if (record) normalized[id] = record;
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
} catch {
|
|
102
|
+
backupInvalidConfig(path);
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function persist(store: CodexAccountStore): void {
|
|
108
|
+
const dir = getConfigDir();
|
|
109
|
+
assertNotRealHomeUnderTest(dir);
|
|
110
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
111
|
+
atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function preservedValidationMetadata(record: CodexAccountCredentialRecord | undefined): Pick<
|
|
115
|
+
CodexAccountCredentialRecord,
|
|
116
|
+
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError"
|
|
117
|
+
> {
|
|
118
|
+
return {
|
|
119
|
+
...(record?.lastCodexValidatedAt !== undefined ? { lastCodexValidatedAt: record.lastCodexValidatedAt } : {}),
|
|
120
|
+
...(record?.lastCodexValidationStatus !== undefined ? { lastCodexValidationStatus: record.lastCodexValidationStatus } : {}),
|
|
121
|
+
...(record?.lastCodexValidationError !== undefined ? { lastCodexValidationError: record.lastCodexValidationError } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getCodexAccountCredential(id: string): CodexAccountCredentials | null {
|
|
126
|
+
const record = readCodexAccountRecord(id);
|
|
127
|
+
if (!record || record.deletedAt != null) return null;
|
|
128
|
+
return record.credential ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function saveCodexAccountCredential(id: string, cred: CodexAccountCredentials): void {
|
|
132
|
+
withCredentialMutationLockSync(() => {
|
|
133
|
+
const store = loadCodexAccountRecordStore();
|
|
134
|
+
const current = store[id];
|
|
135
|
+
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
|
|
136
|
+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
|
|
137
|
+
: refreshGrantFingerprintForToken(cred.refreshToken);
|
|
138
|
+
store[id] = {
|
|
139
|
+
credential: cred,
|
|
140
|
+
generation: (current?.generation ?? 0) + 1,
|
|
141
|
+
refreshGrantFingerprint,
|
|
142
|
+
replacedAt: current ? Date.now() : undefined,
|
|
143
|
+
...preservedValidationMetadata(current),
|
|
144
|
+
};
|
|
145
|
+
persist(store);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
|
|
150
|
+
withCredentialMutationLockSync(() => {
|
|
151
|
+
const store = loadCodexAccountRecordStore();
|
|
152
|
+
const current = store[id];
|
|
153
|
+
if (!current || current.deletedAt != null || !current.credential) return;
|
|
154
|
+
store[id] = {
|
|
155
|
+
...current,
|
|
156
|
+
lastCodexValidatedAt: atMs,
|
|
157
|
+
lastCodexValidationStatus: "ok",
|
|
158
|
+
lastCodexValidationError: undefined,
|
|
159
|
+
};
|
|
160
|
+
persist(store);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function markCodexAccountValidationFailed(id: string, reason: string): void {
|
|
165
|
+
withCredentialMutationLockSync(() => {
|
|
166
|
+
const store = loadCodexAccountRecordStore();
|
|
167
|
+
const current = store[id];
|
|
168
|
+
if (!current || current.deletedAt != null || !current.credential) return;
|
|
169
|
+
store[id] = {
|
|
170
|
+
...current,
|
|
171
|
+
lastCodexValidationStatus: "failed",
|
|
172
|
+
lastCodexValidationError: reason,
|
|
173
|
+
};
|
|
174
|
+
persist(store);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function removeCodexAccountCredential(id: string): void {
|
|
179
|
+
tombstoneCodexAccount(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function listCodexAccountIds(): string[] {
|
|
183
|
+
return Object.keys(loadCodexAccountStore());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function readCodexAccountRecord(id: string): CodexAccountCredentialRecord | null {
|
|
187
|
+
return loadCodexAccountRecordStore()[id] ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isCodexAccountGenerationLive(id: string, generation: number): boolean {
|
|
191
|
+
const record = readCodexAccountRecord(id);
|
|
192
|
+
return !!record?.credential && record.deletedAt == null && record.generation === generation;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function saveCodexAccountCredentialIfGeneration(
|
|
196
|
+
id: string,
|
|
197
|
+
generation: number,
|
|
198
|
+
cred: CodexAccountCredentials,
|
|
199
|
+
): boolean {
|
|
200
|
+
return withCredentialMutationLockSync(() => {
|
|
201
|
+
const store = loadCodexAccountRecordStore();
|
|
202
|
+
const current = store[id];
|
|
203
|
+
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
|
|
207
|
+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
|
|
208
|
+
: refreshGrantFingerprintForToken(cred.refreshToken);
|
|
209
|
+
store[id] = {
|
|
210
|
+
credential: cred,
|
|
211
|
+
generation: generation + 1,
|
|
212
|
+
refreshGrantFingerprint,
|
|
213
|
+
replacedAt: current.replacedAt,
|
|
214
|
+
...preservedValidationMetadata(current),
|
|
215
|
+
};
|
|
216
|
+
persist(store);
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function tombstoneCodexAccount(id: string): number {
|
|
222
|
+
return withCredentialMutationLockSync(() => {
|
|
223
|
+
const store = loadCodexAccountRecordStore();
|
|
224
|
+
const current = store[id];
|
|
225
|
+
const generation = (current?.generation ?? 0) + 1;
|
|
226
|
+
store[id] = { generation, deletedAt: Date.now() };
|
|
227
|
+
persist(store);
|
|
228
|
+
return generation;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
233
|
+
const CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
234
|
+
|
|
235
|
+
export class TokenRefreshError extends Error {
|
|
236
|
+
reason: "expired" | "revoked" | "unknown";
|
|
237
|
+
constructor(reason: "expired" | "revoked" | "unknown", message: string) {
|
|
238
|
+
super(message);
|
|
239
|
+
this.name = "TokenRefreshError";
|
|
240
|
+
this.reason = reason;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class CodexCredentialGenerationConflictError extends Error {
|
|
245
|
+
constructor(message = "Codex account changed during refresh") {
|
|
246
|
+
super(message);
|
|
247
|
+
this.name = "CodexCredentialGenerationConflictError";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export class CodexCredentialRefreshLockTimeoutError extends Error {
|
|
252
|
+
constructor(message = "Timed out waiting for Codex account refresh lock") {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "CodexCredentialRefreshLockTimeoutError";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class CodexCredentialRefreshBusyError extends Error {
|
|
259
|
+
readonly code = "CODEX_REFRESH_BUSY";
|
|
260
|
+
readonly retryable = true;
|
|
261
|
+
|
|
262
|
+
constructor() {
|
|
263
|
+
super("Codex credential refresh capacity reached");
|
|
264
|
+
this.name = "CodexCredentialRefreshBusyError";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export class CodexCredentialRefreshStaleError extends Error {
|
|
269
|
+
readonly code = "CODEX_REFRESH_STALE";
|
|
270
|
+
readonly retryable = true;
|
|
271
|
+
|
|
272
|
+
constructor() {
|
|
273
|
+
super("Codex credential refresh owner became stale");
|
|
274
|
+
this.name = "CodexCredentialRefreshStaleError";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
|
|
279
|
+
function withCredentialMutationLockSync<T>(fn: () => T): T {
|
|
280
|
+
try {
|
|
281
|
+
return withConfigMutationLockSync(fn);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof ConfigMutationLockError) throw new CodexCredentialRefreshLockTimeoutError();
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
|
|
289
|
+
type CodexRefreshResult = CodexTokenResult & { credential?: CodexAccountCredentials };
|
|
290
|
+
const MAX_CODEX_REFRESH_FLIGHTS = 32;
|
|
291
|
+
const CODEX_REFRESH_FLIGHT_STALE_MS = 120_000;
|
|
292
|
+
interface RefreshFlight {
|
|
293
|
+
promise: Promise<CodexRefreshResult>;
|
|
294
|
+
startedAt: number;
|
|
295
|
+
abort: AbortController;
|
|
296
|
+
}
|
|
297
|
+
const refreshLocks = new Map<string, RefreshFlight>();
|
|
298
|
+
|
|
299
|
+
function codexRefreshLockPath(lockKey: string): string {
|
|
300
|
+
const digest = createHash("sha256").update(lockKey).digest("hex").slice(0, 32);
|
|
301
|
+
return join(getConfigDir(), `codex-refresh-${digest}.lock`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
305
|
+
if (!signal) return new Promise(resolve => setTimeout(resolve, ms));
|
|
306
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
signal.removeEventListener("abort", onAbort);
|
|
310
|
+
resolve();
|
|
311
|
+
}, ms);
|
|
312
|
+
const onAbort = () => {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
reject(signal.reason);
|
|
315
|
+
};
|
|
316
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function errCode(err: unknown): string | undefined {
|
|
321
|
+
return err && typeof err === "object" && "code" in err ? String((err as { code?: unknown }).code) : undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isRefreshLockStale(path: string): boolean {
|
|
325
|
+
try {
|
|
326
|
+
hardenExistingSecret(path);
|
|
327
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as { acquiredAt?: unknown };
|
|
328
|
+
return typeof parsed.acquiredAt !== "number" || Date.now() - parsed.acquiredAt > REFRESH_LOCK_STALE_MS;
|
|
329
|
+
} catch {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function withCodexRefreshFileLock<T>(lockKey: string, signal: AbortSignal, fn: () => Promise<T>): Promise<T> {
|
|
335
|
+
hardenConfigDir();
|
|
336
|
+
const dir = getConfigDir();
|
|
337
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
338
|
+
|
|
339
|
+
const path = codexRefreshLockPath(lockKey);
|
|
340
|
+
const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
|
|
341
|
+
let fd: number | null = null;
|
|
342
|
+
while (fd == null) {
|
|
343
|
+
if (signal.aborted) throw signal.reason;
|
|
344
|
+
try {
|
|
345
|
+
fd = openSync(path, "wx", 0o600);
|
|
346
|
+
writeFileSync(fd, JSON.stringify({ acquiredAt: Date.now(), pid: process.pid }) + "\n");
|
|
347
|
+
break;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (errCode(err) !== "EEXIST") throw err;
|
|
350
|
+
if (isRefreshLockStale(path)) {
|
|
351
|
+
try {
|
|
352
|
+
unlinkSync(path);
|
|
353
|
+
} catch (unlinkErr) {
|
|
354
|
+
if (errCode(unlinkErr) !== "ENOENT") throw unlinkErr;
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (Date.now() >= deadline) throw new CodexCredentialRefreshLockTimeoutError();
|
|
359
|
+
await sleep(REFRESH_LOCK_POLL_MS, signal);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
return await fn();
|
|
365
|
+
} finally {
|
|
366
|
+
if (fd != null) closeSync(fd);
|
|
367
|
+
try {
|
|
368
|
+
unlinkSync(path);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (errCode(err) !== "ENOENT") throw err;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function findFreshCredentialForGrant(
|
|
376
|
+
refreshGrantFingerprint: string,
|
|
377
|
+
excludeId: string,
|
|
378
|
+
): CodexAccountCredentials | null {
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
const records = loadCodexAccountRecordStore();
|
|
381
|
+
for (const [candidateId, candidate] of Object.entries(records)) {
|
|
382
|
+
if (candidateId === excludeId || candidate.deletedAt != null || !candidate.credential) continue;
|
|
383
|
+
if (recordGrantFingerprint(candidate) !== refreshGrantFingerprint) continue;
|
|
384
|
+
if (candidate.credential.expiresAt > now + REFRESH_SKEW_MS) return candidate.credential;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function getValidCodexToken(id: string): Promise<CodexTokenResult> {
|
|
390
|
+
const record = readCodexAccountRecord(id);
|
|
391
|
+
const cred = record?.deletedAt == null ? record?.credential : undefined;
|
|
392
|
+
if (!record || !cred) throw new Error("Codex account credential is unavailable; reauthenticate the account.");
|
|
393
|
+
const refreshGrantFingerprint = recordGrantFingerprint(record);
|
|
394
|
+
if (!refreshGrantFingerprint) throw new Error("Codex account credential is unavailable; reauthenticate the account.");
|
|
395
|
+
|
|
396
|
+
if (cred.expiresAt > Date.now() + REFRESH_SKEW_MS) {
|
|
397
|
+
return { accessToken: cred.accessToken, chatgptAccountId: cred.chatgptAccountId, generation: record.generation };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const existing = refreshLocks.get(refreshGrantFingerprint);
|
|
401
|
+
if (existing) {
|
|
402
|
+
if (Date.now() - existing.startedAt > CODEX_REFRESH_FLIGHT_STALE_MS) {
|
|
403
|
+
existing.abort.abort(new CodexCredentialRefreshStaleError());
|
|
404
|
+
if (refreshLocks.get(refreshGrantFingerprint) === existing) refreshLocks.delete(refreshGrantFingerprint);
|
|
405
|
+
} else {
|
|
406
|
+
const refreshed = await existing.promise;
|
|
407
|
+
const current = readCodexAccountRecord(id);
|
|
408
|
+
const currentCred = current?.deletedAt == null ? current?.credential : undefined;
|
|
409
|
+
if (
|
|
410
|
+
current &&
|
|
411
|
+
currentCred &&
|
|
412
|
+
refreshed.credential &&
|
|
413
|
+
recordGrantFingerprint(current) === refreshGrantFingerprint
|
|
414
|
+
) {
|
|
415
|
+
if (!saveCodexAccountCredentialIfGeneration(id, current.generation, refreshed.credential)) {
|
|
416
|
+
throw new CodexCredentialGenerationConflictError();
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
accessToken: refreshed.credential.accessToken,
|
|
420
|
+
chatgptAccountId: refreshed.credential.chatgptAccountId,
|
|
421
|
+
generation: current.generation + 1,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return getValidCodexToken(id);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (refreshLocks.size >= MAX_CODEX_REFRESH_FLIGHTS) throw new CodexCredentialRefreshBusyError();
|
|
429
|
+
|
|
430
|
+
const abort = new AbortController();
|
|
431
|
+
const signal = AbortSignal.any([abort.signal, AbortSignal.timeout(30_000)]);
|
|
432
|
+
let flight!: RefreshFlight;
|
|
433
|
+
const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, signal, async (): Promise<CodexRefreshResult> => {
|
|
434
|
+
const current = readCodexAccountRecord(id);
|
|
435
|
+
const lockedRecord = readCodexAccountRecord(id);
|
|
436
|
+
const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
|
|
437
|
+
if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
|
|
438
|
+
const startGeneration = lockedRecord.generation;
|
|
439
|
+
const lockedRefreshGrantFingerprint = recordGrantFingerprint(lockedRecord);
|
|
440
|
+
if (lockedRefreshGrantFingerprint !== refreshGrantFingerprint) {
|
|
441
|
+
if (lockedCred.expiresAt > Date.now() + REFRESH_SKEW_MS) {
|
|
442
|
+
return {
|
|
443
|
+
accessToken: lockedCred.accessToken,
|
|
444
|
+
chatgptAccountId: lockedCred.chatgptAccountId,
|
|
445
|
+
generation: startGeneration,
|
|
446
|
+
credential: lockedCred,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
throw new CodexCredentialGenerationConflictError();
|
|
450
|
+
}
|
|
451
|
+
if (lockedCred.expiresAt > Date.now() + REFRESH_SKEW_MS) {
|
|
452
|
+
return {
|
|
453
|
+
accessToken: lockedCred.accessToken,
|
|
454
|
+
chatgptAccountId: lockedCred.chatgptAccountId,
|
|
455
|
+
generation: startGeneration,
|
|
456
|
+
credential: lockedCred,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const sameGrantFreshCredential = findFreshCredentialForGrant(refreshGrantFingerprint, id);
|
|
460
|
+
if (sameGrantFreshCredential) {
|
|
461
|
+
if (!saveCodexAccountCredentialIfGeneration(id, startGeneration, sameGrantFreshCredential)) {
|
|
462
|
+
throw new CodexCredentialGenerationConflictError();
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
accessToken: sameGrantFreshCredential.accessToken,
|
|
466
|
+
chatgptAccountId: sameGrantFreshCredential.chatgptAccountId,
|
|
467
|
+
generation: startGeneration + 1,
|
|
468
|
+
credential: sameGrantFreshCredential,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const res = await fetch(CHATGPT_TOKEN_URL, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
474
|
+
body: new URLSearchParams({
|
|
475
|
+
grant_type: "refresh_token",
|
|
476
|
+
client_id: CHATGPT_CLIENT_ID,
|
|
477
|
+
refresh_token: lockedCred.refreshToken,
|
|
478
|
+
}).toString(),
|
|
479
|
+
signal,
|
|
480
|
+
});
|
|
481
|
+
if (!res.ok) {
|
|
482
|
+
const errText = await res.text().catch(() => "");
|
|
483
|
+
let errDesc: string;
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(errText) as { error?: string; error_description?: string };
|
|
486
|
+
errDesc = [parsed.error, parsed.error_description].filter(Boolean).join(": ") || `HTTP ${res.status}`;
|
|
487
|
+
} catch { errDesc = `HTTP ${res.status}`; }
|
|
488
|
+
const reason = errDesc.includes("invalidated") || errDesc.includes("revoked") ? "revoked" as const
|
|
489
|
+
: errDesc.includes("expired") ? "expired" as const
|
|
490
|
+
: "unknown" as const;
|
|
491
|
+
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`);
|
|
492
|
+
}
|
|
493
|
+
const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
494
|
+
|
|
495
|
+
const updated: CodexAccountCredentials = {
|
|
496
|
+
accessToken: data.access_token,
|
|
497
|
+
refreshToken: data.refresh_token ?? lockedCred.refreshToken,
|
|
498
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
499
|
+
chatgptAccountId: lockedCred.chatgptAccountId,
|
|
500
|
+
};
|
|
501
|
+
if (!saveCodexAccountCredentialIfGeneration(id, startGeneration, updated)) {
|
|
502
|
+
throw new CodexCredentialGenerationConflictError();
|
|
503
|
+
}
|
|
504
|
+
return { accessToken: updated.accessToken, chatgptAccountId: updated.chatgptAccountId, generation: startGeneration + 1, credential: updated };
|
|
505
|
+
}).finally(() => {
|
|
506
|
+
if (refreshLocks.get(refreshGrantFingerprint) === flight) refreshLocks.delete(refreshGrantFingerprint);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
flight = { promise: refreshPromise, startedAt: Date.now(), abort };
|
|
510
|
+
refreshLocks.set(refreshGrantFingerprint, flight);
|
|
511
|
+
const result = await refreshPromise;
|
|
512
|
+
return {
|
|
513
|
+
accessToken: result.accessToken,
|
|
514
|
+
chatgptAccountId: result.chatgptAccountId,
|
|
515
|
+
generation: result.generation,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getCodexAccountCredential } from "./account-store";
|
|
2
|
+
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID, isMainAccountTokenLive } from "./main-account";
|
|
4
|
+
import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
|
|
5
|
+
import type { OcxConfig } from "../types";
|
|
6
|
+
|
|
7
|
+
export function isCodexAccountUsable(config: OcxConfig, accountId: string): boolean {
|
|
8
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
9
|
+
// A legacy pool row with the sentinel makes an active `__main__` ambiguous.
|
|
10
|
+
// Fail closed until the authenticated compatibility-delete path removes it.
|
|
11
|
+
if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return false;
|
|
12
|
+
// Main account: credential is the read-only ~/.codex/auth.json token (Option A).
|
|
13
|
+
return isMainAccountTokenLive() && !isAccountNeedsReauth(accountId);
|
|
14
|
+
}
|
|
15
|
+
const exists = (config.codexAccounts ?? [])
|
|
16
|
+
.some(account => isSelectableCodexPoolAccount(account) && account.id === accountId);
|
|
17
|
+
if (!exists) return false;
|
|
18
|
+
if (isAccountNeedsReauth(accountId)) return false;
|
|
19
|
+
return !!getCodexAccountCredential(accountId);
|
|
20
|
+
}
|