@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /v1/alpha/search relay.
|
|
3
|
+
*
|
|
4
|
+
* codex-rs's built-in search client executes CLIENT-SIDE: it POSTs `alpha/search` against the
|
|
5
|
+
* configured base_url with the same ChatGPT bearer auth used for model requests. Under Design B
|
|
6
|
+
* injection base_url is this proxy, so the request otherwise dies on the /v1/* JSON-404 guard.
|
|
7
|
+
* The endpoint is private to the ChatGPT Codex backend, so routed providers and OpenAI API-key
|
|
8
|
+
* providers cannot serve it. Relay the JSON request and response verbatim through the configured
|
|
9
|
+
* ChatGPT forward provider.
|
|
10
|
+
*/
|
|
11
|
+
import { formatErrorResponse } from "../bridge";
|
|
12
|
+
import {
|
|
13
|
+
CodexAccountCooldownError,
|
|
14
|
+
cooldownErrorResponse,
|
|
15
|
+
CodexAuthContextError,
|
|
16
|
+
CodexPoolAuthenticationError,
|
|
17
|
+
CodexThreadAffinityExpiredError,
|
|
18
|
+
} from "../codex/auth-context";
|
|
19
|
+
import { formatCodexProviderForLog } from "../codex/routing";
|
|
20
|
+
import { signalWithTimeout } from "../lib/abort";
|
|
21
|
+
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
22
|
+
import type { OcxConfig } from "../types";
|
|
23
|
+
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar } from "../providers/openai-sidecar";
|
|
24
|
+
import { readJsonRequestBody } from "./request-decompress";
|
|
25
|
+
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
26
|
+
import type { RequestLogContext } from "./request-log";
|
|
27
|
+
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default TOTAL deadline for one search relay. alpha/search is non-streaming JSON — response
|
|
31
|
+
* headers arrive only when the search finishes — so the budget must cover the whole request.
|
|
32
|
+
* Overridable via config.search.timeoutMs; never config.connectTimeoutMs, whose documented
|
|
33
|
+
* contract is the DNS/TCP/TLS/header-arrival budget (a 10s connect budget would kill every
|
|
34
|
+
* long-running search).
|
|
35
|
+
*/
|
|
36
|
+
const SEARCH_UPSTREAM_TIMEOUT_MS = 200_000;
|
|
37
|
+
const SEARCH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
export async function handleSearch(
|
|
40
|
+
req: Request,
|
|
41
|
+
config: OcxConfig,
|
|
42
|
+
logCtx: RequestLogContext,
|
|
43
|
+
): Promise<Response> {
|
|
44
|
+
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
let body: unknown;
|
|
50
|
+
try {
|
|
51
|
+
body = await readJsonRequestBody(req);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return decodeRequestErrorResponse(err, "search");
|
|
54
|
+
}
|
|
55
|
+
const model = (body as { model?: unknown } | null)?.model;
|
|
56
|
+
if (typeof model === "string" && model) logCtx.model = model;
|
|
57
|
+
|
|
58
|
+
const candidates = listOpenAiForwardSidecarCandidates(config);
|
|
59
|
+
if (candidates.length === 0) {
|
|
60
|
+
return formatErrorResponse(
|
|
61
|
+
400,
|
|
62
|
+
"invalid_request_error",
|
|
63
|
+
"Built-in web search needs a ChatGPT forward provider, but none is configured in opencodex. "
|
|
64
|
+
+ "Routed and OpenAI API-key providers cannot serve /v1/alpha/search.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let upstream: Awaited<ReturnType<typeof resolveFirstUsableOpenAiSidecar>>;
|
|
69
|
+
try {
|
|
70
|
+
upstream = await resolveFirstUsableOpenAiSidecar(candidates, req.headers, config);
|
|
71
|
+
if (!upstream) {
|
|
72
|
+
return formatErrorResponse(
|
|
73
|
+
401,
|
|
74
|
+
"authentication_error",
|
|
75
|
+
"web search relay needs ChatGPT auth (Authorization header)",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
logCtx.provider = formatCodexProviderForLog(upstream.providerName, codexLogAccountId(upstream.authContext), config);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
81
|
+
return cooldownErrorResponse(err);
|
|
82
|
+
}
|
|
83
|
+
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
84
|
+
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
85
|
+
}
|
|
86
|
+
if (err instanceof CodexAuthContextError) {
|
|
87
|
+
const safeAccountLabel = formatCodexProviderForLog("openai", err.accountId, config);
|
|
88
|
+
console.error(`[search] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
89
|
+
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
90
|
+
}
|
|
91
|
+
if (err instanceof CodexPoolAuthenticationError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
96
|
+
if (upstream.provider.headers) Object.assign(headers, upstream.provider.headers);
|
|
97
|
+
for (const [name, value] of upstream.headers) headers[name] = value;
|
|
98
|
+
const url = `${upstream.provider.baseUrl}/alpha/search`;
|
|
99
|
+
const timeoutMs = config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS;
|
|
100
|
+
const linkedSignal = signalWithTimeout(timeoutMs, req.signal);
|
|
101
|
+
const sidecarExit = sidecarEnter("search");
|
|
102
|
+
try {
|
|
103
|
+
const upstreamResponse = await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
signal: linkedSignal.signal,
|
|
108
|
+
});
|
|
109
|
+
const payload = await upstreamResponse.arrayBuffer();
|
|
110
|
+
if (payload.byteLength > SEARCH_RESPONSE_MAX_BYTES) {
|
|
111
|
+
return formatErrorResponse(502, "upstream_error", `search response too large (${payload.byteLength} bytes)`);
|
|
112
|
+
}
|
|
113
|
+
upstream.recordOutcome?.(upstreamResponse.status);
|
|
114
|
+
const relayHeaders: Record<string, string> = {};
|
|
115
|
+
const contentType = upstreamResponse.headers.get("content-type");
|
|
116
|
+
if (contentType) relayHeaders["content-type"] = contentType;
|
|
117
|
+
return new Response(payload, { status: upstreamResponse.status, headers: relayHeaders });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (req.signal.aborted) {
|
|
120
|
+
return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
|
|
121
|
+
}
|
|
122
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
123
|
+
upstream.recordOutcome?.("timeout");
|
|
124
|
+
return formatErrorResponse(504, "upstream_error", "search upstream timed out");
|
|
125
|
+
}
|
|
126
|
+
upstream.recordOutcome?.("connect_error");
|
|
127
|
+
return formatErrorResponse(
|
|
128
|
+
502,
|
|
129
|
+
"upstream_error",
|
|
130
|
+
`search relay failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
|
+
);
|
|
132
|
+
} finally {
|
|
133
|
+
sidecarExit();
|
|
134
|
+
linkedSignal.cleanup();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared client-facing SSE payload rewrite shell.
|
|
5
|
+
*
|
|
6
|
+
* Multiple opt-in transforms (image-gen namespace restore, item-id repair, …) compose into one
|
|
7
|
+
* parse/stringify pass so a tee'd stream is not re-framed twice per event.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SsePayloadRewrite = (payload: string) => string;
|
|
11
|
+
|
|
12
|
+
/** Split one complete SSE event block while retaining its original blank-line delimiter. */
|
|
13
|
+
export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
|
|
14
|
+
const match = buffer.match(/\r?\n\r?\n/);
|
|
15
|
+
if (!match || match.index === undefined) return null;
|
|
16
|
+
return {
|
|
17
|
+
block: buffer.slice(0, match.index),
|
|
18
|
+
delimiter: match[0],
|
|
19
|
+
rest: buffer.slice(match.index + match[0].length),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Join all data lines from one SSE event according to the event-stream field rules. */
|
|
24
|
+
export function sseDataPayload(block: string): string | null {
|
|
25
|
+
const data: string[] = [];
|
|
26
|
+
for (const line of block.split(/\r?\n/)) {
|
|
27
|
+
if (!line.startsWith("data:")) continue;
|
|
28
|
+
const value = line.slice(5);
|
|
29
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
30
|
+
}
|
|
31
|
+
return data.length > 0 ? data.join("\n") : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Replace an SSE event's data field while preserving non-data fields and newline style. */
|
|
35
|
+
export function replaceSseDataPayload(block: string, payload: string): string {
|
|
36
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
37
|
+
const lines = block.split(/\r?\n/);
|
|
38
|
+
const rewritten: string[] = [];
|
|
39
|
+
let replaced = false;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (!line.startsWith("data:")) {
|
|
42
|
+
rewritten.push(line);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!replaced) {
|
|
46
|
+
rewritten.push(`data: ${payload}`);
|
|
47
|
+
replaced = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return replaced ? rewritten.join(newline) : block;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Apply rewrites left-to-right; empty list is identity. */
|
|
54
|
+
export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): SsePayloadRewrite {
|
|
55
|
+
if (rewrites.length === 0) return (payload) => payload;
|
|
56
|
+
if (rewrites.length === 1) return rewrites[0]!;
|
|
57
|
+
return (payload) => {
|
|
58
|
+
let next = payload;
|
|
59
|
+
for (const rewrite of rewrites) next = rewrite(next);
|
|
60
|
+
return next;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Relay an SSE body through a single JS pull wrapper, rewriting each event's data payload in place.
|
|
66
|
+
* Non-data fields and framing are preserved; invalid JSON payloads are left to the rewrite callback.
|
|
67
|
+
*/
|
|
68
|
+
export function relaySseWithPayloadRewrite(
|
|
69
|
+
body: ReadableStream<Uint8Array>,
|
|
70
|
+
rewrite: SsePayloadRewrite,
|
|
71
|
+
translatorBudget: TranslatorBudget,
|
|
72
|
+
): ReadableStream<Uint8Array> {
|
|
73
|
+
const reader = body.getReader();
|
|
74
|
+
const decoder = new TextDecoder();
|
|
75
|
+
const encoder = new TextEncoder();
|
|
76
|
+
let buffer = "";
|
|
77
|
+
let bufferBytes = 0;
|
|
78
|
+
|
|
79
|
+
const appendBuffer = (fragment: string): void => {
|
|
80
|
+
if (!fragment) return;
|
|
81
|
+
const nextBytes = bufferBytes + encoder.encode(fragment).byteLength;
|
|
82
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
83
|
+
try {
|
|
84
|
+
buffer += fragment;
|
|
85
|
+
reservation.commitRetained();
|
|
86
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
87
|
+
bufferBytes = nextBytes;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
reservation.release();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const replaceBuffer = (next: string): void => {
|
|
95
|
+
const nextBytes = encoder.encode(next).byteLength;
|
|
96
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
97
|
+
reservation.commitRetained();
|
|
98
|
+
buffer = next;
|
|
99
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
100
|
+
bufferBytes = nextBytes;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const enqueueText = (
|
|
104
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
105
|
+
text: string,
|
|
106
|
+
): void => {
|
|
107
|
+
const bytes = encoder.encode(text).byteLength;
|
|
108
|
+
const reservation = translatorBudget.reserveTransient(bytes, { kind: "live_transient" });
|
|
109
|
+
try {
|
|
110
|
+
const encoded = encoder.encode(text);
|
|
111
|
+
reservation.commitRetained();
|
|
112
|
+
controller.enqueue(encoded);
|
|
113
|
+
translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
reservation.release();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const releaseBuffer = (): void => {
|
|
121
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
122
|
+
buffer = "";
|
|
123
|
+
bufferBytes = 0;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const emitProcessedBlocks = (
|
|
127
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
128
|
+
flushFinal = false,
|
|
129
|
+
): void => {
|
|
130
|
+
let next: { block: string; delimiter: string; rest: string } | null;
|
|
131
|
+
while ((next = nextSseBlock(buffer))) {
|
|
132
|
+
replaceBuffer(next.rest);
|
|
133
|
+
const payload = sseDataPayload(next.block);
|
|
134
|
+
const rewrittenPayload = payload ? rewrite(payload) : undefined;
|
|
135
|
+
const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
|
|
136
|
+
? replaceSseDataPayload(next.block, rewrittenPayload)
|
|
137
|
+
: next.block;
|
|
138
|
+
enqueueText(controller, block + next.delimiter);
|
|
139
|
+
}
|
|
140
|
+
if (flushFinal && buffer.length > 0) {
|
|
141
|
+
const payload = sseDataPayload(buffer);
|
|
142
|
+
const rewrittenPayload = payload ? rewrite(payload) : undefined;
|
|
143
|
+
const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
|
|
144
|
+
? replaceSseDataPayload(buffer, rewrittenPayload)
|
|
145
|
+
: buffer;
|
|
146
|
+
enqueueText(controller, block);
|
|
147
|
+
releaseBuffer();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return new ReadableStream<Uint8Array>({
|
|
152
|
+
async pull(controller) {
|
|
153
|
+
try {
|
|
154
|
+
const { done, value } = await reader.read();
|
|
155
|
+
if (done) {
|
|
156
|
+
appendBuffer(decoder.decode());
|
|
157
|
+
emitProcessedBlocks(controller, true);
|
|
158
|
+
releaseBuffer();
|
|
159
|
+
controller.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
appendBuffer(decoder.decode(value, { stream: true }));
|
|
163
|
+
emitProcessedBlocks(controller);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
releaseBuffer();
|
|
166
|
+
try { await reader.cancel(error); } catch { /* already closed */ }
|
|
167
|
+
controller.error(error);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
cancel(reason) {
|
|
171
|
+
releaseBuffer();
|
|
172
|
+
reader.cancel(reason).catch(() => {});
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { durableBunPath } from "../lib/bun-runtime";
|
|
5
|
+
import {
|
|
6
|
+
WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER,
|
|
7
|
+
isWindowsSchtasksCreateAccessDenied,
|
|
8
|
+
} from "../lib/windows-elevation";
|
|
9
|
+
import {
|
|
10
|
+
finalizeWindowsSchedulerServiceRegistration,
|
|
11
|
+
type ElevatedReconciliationOutcome,
|
|
12
|
+
type FinalizeWindowsSchedulerOptions,
|
|
13
|
+
type FinalizeWindowsSchedulerResult,
|
|
14
|
+
type WindowsSchedulerTaskProbe,
|
|
15
|
+
} from "../service";
|
|
16
|
+
|
|
17
|
+
export type StartupInstallAction = "install-service" | "install-shim";
|
|
18
|
+
|
|
19
|
+
export type StartupInstallState =
|
|
20
|
+
| { status: "idle" }
|
|
21
|
+
| {
|
|
22
|
+
status: "running";
|
|
23
|
+
action: StartupInstallAction;
|
|
24
|
+
attemptId: string;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
status: "indeterminate";
|
|
29
|
+
action: "install-service";
|
|
30
|
+
attemptId: string;
|
|
31
|
+
startedAt: number;
|
|
32
|
+
timedOutAt: number;
|
|
33
|
+
reconciliation: Promise<ElevatedReconciliationOutcome>;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
status: "blocked";
|
|
37
|
+
reason: "partial-elevated-install";
|
|
38
|
+
action: "install-service";
|
|
39
|
+
attemptId: string;
|
|
40
|
+
detail: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const INSTALL_DETAIL_LIMIT = 2_000;
|
|
44
|
+
const INDETERMINATE_MESSAGE =
|
|
45
|
+
"Windows elevation timed out, but the elevated Task Scheduler transaction may still be running. New service installation attempts are temporarily blocked while OpenCodex reconciles its final state.";
|
|
46
|
+
const RECONCILING_MESSAGE =
|
|
47
|
+
"A previous elevated Windows service installation is still being reconciled. Wait for it to finish or inspect the Task Scheduler state before retrying.";
|
|
48
|
+
const BLOCKED_PARTIAL_MESSAGE =
|
|
49
|
+
"A previous elevated Windows service installation left a partial Task Scheduler state. Remove the OpenCodex scheduler task (or confirm it is absent), then clear the install block before retrying.";
|
|
50
|
+
|
|
51
|
+
let installState: StartupInstallState = { status: "idle" };
|
|
52
|
+
|
|
53
|
+
type FinalizeFn = (
|
|
54
|
+
script?: string,
|
|
55
|
+
options?: FinalizeWindowsSchedulerOptions,
|
|
56
|
+
) => Promise<FinalizeWindowsSchedulerResult>;
|
|
57
|
+
let finalizeImpl: FinalizeFn = finalizeWindowsSchedulerServiceRegistration;
|
|
58
|
+
|
|
59
|
+
/** Test-only seam so elevation retry tests do not mock.module the service package. */
|
|
60
|
+
export function setStartupInstallFinalizeForTests(next: FinalizeFn | null): void {
|
|
61
|
+
finalizeImpl = next ?? finalizeWindowsSchedulerServiceRegistration;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Snapshot of the install lock for diagnostics and tests. */
|
|
65
|
+
export function getStartupInstallState(): StartupInstallState {
|
|
66
|
+
return installState;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when this attempt still owns the active/indeterminate/blocked lock. */
|
|
70
|
+
export function ownsStartupInstallAttempt(attemptId: string): boolean {
|
|
71
|
+
return (installState.status === "running"
|
|
72
|
+
|| installState.status === "indeterminate"
|
|
73
|
+
|| installState.status === "blocked")
|
|
74
|
+
&& installState.attemptId === attemptId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Clear a partial-install block after the operator has cleaned up Task Scheduler.
|
|
79
|
+
* Requires an observed probe and clears only when the task is proven absent.
|
|
80
|
+
* Does not itself delete the task.
|
|
81
|
+
*/
|
|
82
|
+
export function clearStartupInstallPartialBlock(options: {
|
|
83
|
+
probe: WindowsSchedulerTaskProbe;
|
|
84
|
+
}): { cleared: boolean; detail: string } {
|
|
85
|
+
if (installState.status !== "blocked") {
|
|
86
|
+
return { cleared: false, detail: `Install lock is ${installState.status}, not blocked.` };
|
|
87
|
+
}
|
|
88
|
+
if (options.probe.status === "present") {
|
|
89
|
+
return {
|
|
90
|
+
cleared: false,
|
|
91
|
+
detail: "OpenCodex Task Scheduler task is still present; remove it before clearing the block.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (options.probe.status === "unknown") {
|
|
95
|
+
return {
|
|
96
|
+
cleared: false,
|
|
97
|
+
detail: `Could not verify Task Scheduler absence (${options.probe.detail}); not clearing the block.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
installState = { status: "idle" };
|
|
101
|
+
return { cleared: true, detail: "Partial-install block cleared." };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Test-only reset of the install lock. */
|
|
105
|
+
export function resetStartupInstallStateForTests(): void {
|
|
106
|
+
installState = { status: "idle" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function startupInstallArgv(
|
|
110
|
+
action: StartupInstallAction,
|
|
111
|
+
options?: { repair?: boolean },
|
|
112
|
+
): string[] {
|
|
113
|
+
if (action === "install-service") {
|
|
114
|
+
return options?.repair ? ["service", "repair"] : ["service", "install"];
|
|
115
|
+
}
|
|
116
|
+
return ["codex-shim", "install"];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface CliInstallFailure {
|
|
120
|
+
/** Machine marker such as OCX_ERROR_CODE=..., when present in any stream. */
|
|
121
|
+
code: string | null;
|
|
122
|
+
stdout: string;
|
|
123
|
+
stderr: string;
|
|
124
|
+
message: string;
|
|
125
|
+
/** Bounded user-facing diagnostic (may append the marker if truncated away). */
|
|
126
|
+
detail: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractOcxErrorCode(text: string): string | null {
|
|
130
|
+
const match = text.match(/OCX_ERROR_CODE=[A-Z0-9_]+/);
|
|
131
|
+
return match ? match[0] : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Classify a CLI install failure, keeping the machine marker separate from the
|
|
136
|
+
* truncated user-facing diagnostic.
|
|
137
|
+
*/
|
|
138
|
+
export function classifyCliInstallFailure(stdout: string, stderr: string, error: Error): CliInstallFailure {
|
|
139
|
+
const stdoutText = stdout.trim();
|
|
140
|
+
const stderrText = stderr.trim();
|
|
141
|
+
const message = error.message.trim();
|
|
142
|
+
const combined = [stderrText, stdoutText, message].filter(Boolean).join("\n");
|
|
143
|
+
const code = extractOcxErrorCode(combined);
|
|
144
|
+
let detail = combined || message;
|
|
145
|
+
if (detail.length > INSTALL_DETAIL_LIMIT) {
|
|
146
|
+
const truncated = detail.slice(0, INSTALL_DETAIL_LIMIT);
|
|
147
|
+
detail = code && !truncated.includes(code)
|
|
148
|
+
? `${truncated}\n${code}… (truncated)`
|
|
149
|
+
: `${truncated}… (truncated)`;
|
|
150
|
+
}
|
|
151
|
+
return { code, stdout: stdoutText, stderr: stderrText, message, detail };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @deprecated Prefer classifyCliInstallFailure; kept for existing tests. */
|
|
155
|
+
export function installFailureDetail(stdout: string, stderr: string, error: Error): string {
|
|
156
|
+
return classifyCliInstallFailure(stdout, stderr, error).detail;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runCliInstall(
|
|
160
|
+
action: StartupInstallAction,
|
|
161
|
+
options?: { repair?: boolean },
|
|
162
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
163
|
+
const bun = durableBunPath();
|
|
164
|
+
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
165
|
+
const argv = [cli, ...startupInstallArgv(action, options)];
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
execFile(bun, argv, {
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
env: process.env,
|
|
170
|
+
timeout: 60_000,
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
maxBuffer: 256 * 1024,
|
|
173
|
+
}, (error, stdout, stderr) => {
|
|
174
|
+
if (error) {
|
|
175
|
+
const failure = classifyCliInstallFailure(stdout, stderr, error);
|
|
176
|
+
reject(Object.assign(new Error(failure.detail), { ocxInstallFailure: failure }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
resolve({ stdout, stderr });
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function installFailureCode(error: unknown): string | null {
|
|
185
|
+
if (error && typeof error === "object" && "ocxInstallFailure" in error) {
|
|
186
|
+
const failure = (error as { ocxInstallFailure?: CliInstallFailure }).ocxInstallFailure;
|
|
187
|
+
if (failure?.code) return failure.code;
|
|
188
|
+
}
|
|
189
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
190
|
+
return extractOcxErrorCode(detail);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function rejectIfBusy(_action: StartupInstallAction): Error | null {
|
|
194
|
+
if (installState.status === "running") {
|
|
195
|
+
return new Error(`Another startup installation is already running: ${installState.action}`);
|
|
196
|
+
}
|
|
197
|
+
if (installState.status === "indeterminate") {
|
|
198
|
+
return new Error(RECONCILING_MESSAGE);
|
|
199
|
+
}
|
|
200
|
+
if (installState.status === "blocked") {
|
|
201
|
+
return new Error(BLOCKED_PARTIAL_MESSAGE);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applyReconciliationOutcome(
|
|
207
|
+
attemptId: string,
|
|
208
|
+
outcome: ElevatedReconciliationOutcome,
|
|
209
|
+
): void {
|
|
210
|
+
if (installState.status !== "indeterminate" || installState.attemptId !== attemptId) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (outcome === "blocked-partial") {
|
|
214
|
+
installState = {
|
|
215
|
+
status: "blocked",
|
|
216
|
+
reason: "partial-elevated-install",
|
|
217
|
+
action: "install-service",
|
|
218
|
+
attemptId,
|
|
219
|
+
detail: BLOCKED_PARTIAL_MESSAGE,
|
|
220
|
+
};
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
installState = { status: "idle" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Execute the existing fixed CLI installer outside the proxy event loop.
|
|
228
|
+
*
|
|
229
|
+
* Repair mode (`options.repair`) runs `ocx service repair` — asset rewrite + restart
|
|
230
|
+
* without Task Scheduler re-registration, so it must not enter the UAC elevation path.
|
|
231
|
+
*
|
|
232
|
+
* After an elevation request timeout the lock becomes `indeterminate` until the
|
|
233
|
+
* original elevated transaction completes and is reconciled. A process restart
|
|
234
|
+
* clears this in-memory lock — callers must then inspect Task Scheduler reality
|
|
235
|
+
* (see evaluateSchedulerInstallRestartReconciliation) before installing again.
|
|
236
|
+
*/
|
|
237
|
+
export function runStartupInstallAction(
|
|
238
|
+
action: StartupInstallAction,
|
|
239
|
+
options?: { repair?: boolean },
|
|
240
|
+
): Promise<{ message: string }> {
|
|
241
|
+
const busy = rejectIfBusy(action);
|
|
242
|
+
if (busy) return Promise.reject(busy);
|
|
243
|
+
|
|
244
|
+
const repair = options?.repair === true;
|
|
245
|
+
const attemptId = randomUUID();
|
|
246
|
+
const startedAt = Date.now();
|
|
247
|
+
installState = { status: "running", action, attemptId, startedAt };
|
|
248
|
+
|
|
249
|
+
const operation = (async () => {
|
|
250
|
+
try {
|
|
251
|
+
await runCliInstall(action, { repair });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const code = installFailureCode(error);
|
|
254
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
255
|
+
// Elevate only for fresh install + structured Task Scheduler /create access denial —
|
|
256
|
+
// never for repair, WinSW removal, asset writes, or generic permission errors.
|
|
257
|
+
if (
|
|
258
|
+
!repair
|
|
259
|
+
&& action === "install-service"
|
|
260
|
+
&& process.platform === "win32"
|
|
261
|
+
&& (code === WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER
|
|
262
|
+
|| isWindowsSchtasksCreateAccessDenied(detail))
|
|
263
|
+
) {
|
|
264
|
+
const finalized = await finalizeImpl(undefined, {
|
|
265
|
+
attemptId,
|
|
266
|
+
stillOwnsAttempt: ownsStartupInstallAttempt,
|
|
267
|
+
});
|
|
268
|
+
if (finalized.kind === "indeterminate") {
|
|
269
|
+
const ownedAttemptId = finalized.attemptId;
|
|
270
|
+
installState = {
|
|
271
|
+
status: "indeterminate",
|
|
272
|
+
action: "install-service",
|
|
273
|
+
attemptId: ownedAttemptId,
|
|
274
|
+
startedAt,
|
|
275
|
+
timedOutAt: Date.now(),
|
|
276
|
+
reconciliation: finalized.reconciliation,
|
|
277
|
+
};
|
|
278
|
+
void finalized.reconciliation.then(
|
|
279
|
+
outcome => {
|
|
280
|
+
applyReconciliationOutcome(ownedAttemptId, outcome);
|
|
281
|
+
},
|
|
282
|
+
() => {
|
|
283
|
+
// Fail closed: a rejected reconciliation must not leave an unhandled rejection
|
|
284
|
+
// or silently idle the lock while Task Scheduler state may still be partial.
|
|
285
|
+
applyReconciliationOutcome(ownedAttemptId, "blocked-partial");
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
throw new Error(INDETERMINATE_MESSAGE);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (action === "install-service") {
|
|
295
|
+
return { message: repair ? "Background service repaired." : "Background service installed." };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
message: repair ? "Codex launcher shim repaired." : "Codex launcher shim installed.",
|
|
299
|
+
};
|
|
300
|
+
})();
|
|
301
|
+
|
|
302
|
+
return operation.finally(() => {
|
|
303
|
+
// Do not clear an indeterminate or blocked lock from a timed-out elevation.
|
|
304
|
+
if (installState.status === "running" && installState.attemptId === attemptId) {
|
|
305
|
+
installState = { status: "idle" };
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|