@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
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
4
|
+
import { stripGrokConfig } from "../grok/inject";
|
|
5
|
+
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
6
|
+
import { reconcileJournal } from "../codex/journal";
|
|
7
|
+
import {
|
|
8
|
+
codexAutoStartEnabled,
|
|
9
|
+
getConfigDir,
|
|
10
|
+
loadConfig,
|
|
11
|
+
readPid,
|
|
12
|
+
readPidFileValue,
|
|
13
|
+
readRuntimePort,
|
|
14
|
+
removePid,
|
|
15
|
+
removePidIfValueIs,
|
|
16
|
+
removeRuntimePort,
|
|
17
|
+
removeRuntimePortIfPidIs,
|
|
18
|
+
saveConfig,
|
|
19
|
+
writePid,
|
|
20
|
+
writeRuntimePort,
|
|
21
|
+
} from "../config";
|
|
22
|
+
import { collectStatus } from "./status";
|
|
23
|
+
import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch";
|
|
24
|
+
import { runTrayProxyRestart, runTrayProxyStart } from "./tray-proxy";
|
|
25
|
+
import { installCrashGuards } from "../lib/crash-guard";
|
|
26
|
+
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
|
|
27
|
+
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
28
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
29
|
+
import { stopProxy } from "../lib/process-control";
|
|
30
|
+
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
31
|
+
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
32
|
+
import { startupHealthSummary } from "../codex/autostart-health";
|
|
33
|
+
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
34
|
+
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
35
|
+
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
36
|
+
import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
37
|
+
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
38
|
+
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
39
|
+
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
40
|
+
import { maybeShowStarPrompt } from "./star-prompt";
|
|
41
|
+
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
42
|
+
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
43
|
+
import { syncModelsToCodex } from "../codex/sync";
|
|
44
|
+
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
45
|
+
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
46
|
+
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
47
|
+
|
|
48
|
+
const args = process.argv.slice(2);
|
|
49
|
+
const command = args[0];
|
|
50
|
+
|
|
51
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
52
|
+
printVersion();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
57
|
+
if (command === "help" && args[1]) printSubcommandUsage(args[1]);
|
|
58
|
+
else printUsage();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
63
|
+
printSubcommandUsage(command);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
maybeAutoRestoreCodexShim(command, args);
|
|
68
|
+
|
|
69
|
+
function parsePortOption(): number | undefined {
|
|
70
|
+
if (args.length === 1) return undefined;
|
|
71
|
+
if (args.length !== 3 || args[1] !== "--port") {
|
|
72
|
+
console.error("Usage: ocx start [--port <port>]");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const portIdx = args.indexOf("--port");
|
|
76
|
+
if (portIdx === -1) return undefined;
|
|
77
|
+
const value = args[portIdx + 1];
|
|
78
|
+
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
79
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
80
|
+
console.error("Invalid port number");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
return port;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
87
|
+
const deadline = Date.now() + timeoutMs;
|
|
88
|
+
while (Date.now() < deadline) {
|
|
89
|
+
// Runtime-state-first with identity: finds the proxy even when it started on a
|
|
90
|
+
// fallback port, and never mistakes a foreign 200 for our proxy.
|
|
91
|
+
const live = await findLiveProxy();
|
|
92
|
+
if (live) return live;
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A Grok fence sync that throws is best-effort by design — it must never block startup.
|
|
100
|
+
* Reporting nothing, however, is what lets a STALE fence survive: `~/.grok/config.toml`
|
|
101
|
+
* keeps naming whatever port the last successful sync wrote, and once that listener is
|
|
102
|
+
* gone every grok turn retries against a refused connection while our own log stays
|
|
103
|
+
* silent (2026-07-27 field report: 8 entries pinned to a dead 127.0.0.1:4179).
|
|
104
|
+
* So say what failed and name the single command that repairs it.
|
|
105
|
+
*/
|
|
106
|
+
function grokSyncFailureMessage(err: unknown): string {
|
|
107
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
108
|
+
return `Grok Build config sync failed: ${detail}. `
|
|
109
|
+
+ "~/.grok/config.toml may still point at a previous proxy port — "
|
|
110
|
+
+ "run 'ocx ensure' (or apply from the dashboard's Grok page) to repoint it.";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
114
|
+
function startArgv(port?: number): string[] {
|
|
115
|
+
const args = [process.argv[1], "start"];
|
|
116
|
+
if (typeof port === "number" && Number.isFinite(port) && port > 0 && port <= 65535) {
|
|
117
|
+
args.push("--port", String(Math.trunc(port)));
|
|
118
|
+
}
|
|
119
|
+
return args;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
const preferred = requestedPort ?? config.port ?? 10100;
|
|
125
|
+
const hardPin = requestedPort !== undefined && requestedPort > 0;
|
|
126
|
+
// Soft start: brief prefer-retry then ephemeral hop.
|
|
127
|
+
// Explicit `--port` (service wrappers / update restart): wait for the pinned port
|
|
128
|
+
// to free without killing any listener (healthy ocx / foreign). Never hop.
|
|
129
|
+
if (hardPin && preferred > 0) {
|
|
130
|
+
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
131
|
+
await reclaimListenPort(preferred, config.hostname ?? "127.0.0.1", {
|
|
132
|
+
// Ghost LISTEN rows with a dead PID can outlive the process for a while.
|
|
133
|
+
// SetTcpEntry(DELETE_TCB) needs elevation (often returns 317), so the only
|
|
134
|
+
// reliable non-admin recovery is to wait for the OS to release the TCB.
|
|
135
|
+
timeoutMs: 60_000,
|
|
136
|
+
intervalMs: 100,
|
|
137
|
+
scanIntervalMs: 500,
|
|
138
|
+
killOcxHolders: false,
|
|
139
|
+
dropTcpRows: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1", {
|
|
144
|
+
// After reclaim, keep probing briefly — ghost rows sometimes clear between
|
|
145
|
+
// the reclaim deadline and the final listen. Still never hop off `--port`.
|
|
146
|
+
preferRetryMs: hardPin ? 5_000 : 750,
|
|
147
|
+
preferRetryIntervalMs: 50,
|
|
148
|
+
allowEphemeralFallback: !hardPin,
|
|
149
|
+
});
|
|
150
|
+
if (preferred > 0 && selected !== preferred) {
|
|
151
|
+
console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
|
|
152
|
+
}
|
|
153
|
+
if (shouldPersistSelectedPort(config.port, selected, preferred)) {
|
|
154
|
+
config.port = selected;
|
|
155
|
+
saveConfig(config);
|
|
156
|
+
}
|
|
157
|
+
return selected;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof PortUnavailableError) {
|
|
160
|
+
console.error(`❌ ${err.message}`);
|
|
161
|
+
console.error(" Stop whatever holds that port, or change config.port, then retry.");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleStart(options: { block?: boolean } = {}) {
|
|
169
|
+
// Native (WinSW) service mode has no batch wrapper to read the service token file
|
|
170
|
+
// into the environment, so the app loads it here before the server binds. The server
|
|
171
|
+
// auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
|
|
172
|
+
const serviceToken = loadServiceTokenFromFile(process.env);
|
|
173
|
+
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
174
|
+
const requestedPort = parsePortOption();
|
|
175
|
+
if (!currentExternalCodexModelProvider()) reconcileJournal();
|
|
176
|
+
const existingPid = readPid();
|
|
177
|
+
if (existingPid) {
|
|
178
|
+
const live = await findLiveProxy();
|
|
179
|
+
if (live) {
|
|
180
|
+
console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'ocx stop' first.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
removePid(existingPid);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
187
|
+
// PID: choosing "Update now" installs globally and exits, so we never want a
|
|
188
|
+
// live daemon holding resources while it overwrites its own binary.
|
|
189
|
+
await maybeShowUpdatePrompt();
|
|
190
|
+
|
|
191
|
+
// Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
|
|
192
|
+
// between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
|
|
193
|
+
// the same port only (never hop — that was the remaining PR #152 gap).
|
|
194
|
+
let port = await chooseListenPort(requestedPort);
|
|
195
|
+
let server: ReturnType<typeof startServer>;
|
|
196
|
+
for (let attempt = 0; ; attempt++) {
|
|
197
|
+
try {
|
|
198
|
+
server = startServer(port);
|
|
199
|
+
// Prewarm the live provider model cache as soon as the port is bound so the
|
|
200
|
+
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
|
|
201
|
+
// instead of racing duplicate upstream /models fetches.
|
|
202
|
+
scheduleCatalogPrewarm();
|
|
203
|
+
break;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (!isAddrInUse(err) || attempt >= 2) throw err;
|
|
206
|
+
if (requestedPort !== undefined) {
|
|
207
|
+
console.log(`⚠️ Port ${port} was taken while starting; waiting to retry the same port...`);
|
|
208
|
+
const hostname = loadConfig().hostname ?? "127.0.0.1";
|
|
209
|
+
const freed = await waitForPortAvailable(port, hostname, { timeoutMs: 3_000, intervalMs: 50 });
|
|
210
|
+
if (!freed) {
|
|
211
|
+
console.error(`❌ Port ${port} stayed busy; refusing to hop to an ephemeral port.`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
console.log(`⚠️ Port ${port} was taken while starting; picking another...`);
|
|
217
|
+
port = await chooseListenPort(requestedPort);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// A single request's streaming error must never crash the daemon serving every
|
|
221
|
+
// other Codex session — capture the full stack to crash.log and stay up.
|
|
222
|
+
installCrashGuards();
|
|
223
|
+
writePid(process.pid);
|
|
224
|
+
|
|
225
|
+
const config = loadConfig();
|
|
226
|
+
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
227
|
+
// No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
|
|
228
|
+
// is about to transform; snapshotting earlier only captured a baseline that could
|
|
229
|
+
// already be stale by the time injection ran (#477).
|
|
230
|
+
|
|
231
|
+
// Background proactive token refresh. No-op unless config.tokenGuardian.enabled; timer is unref'd
|
|
232
|
+
// so it never keeps the process alive on its own. Stopped in syncCleanup so no refresh fires mid-drain.
|
|
233
|
+
const guardian = startTokenGuardian();
|
|
234
|
+
// Design B upgrade path: keep retrying the one-time opencodex→openai history migration in the
|
|
235
|
+
// background — the first `ocx start` after an update usually races the Codex app's DB lock.
|
|
236
|
+
// Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
|
|
237
|
+
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
238
|
+
|
|
239
|
+
let cleaned = false;
|
|
240
|
+
let cleanupSucceeded = true;
|
|
241
|
+
const syncCleanup = () => {
|
|
242
|
+
if (cleaned) return cleanupSucceeded;
|
|
243
|
+
cleaned = true;
|
|
244
|
+
try { guardian.stop(); } catch { /* best-effort */ }
|
|
245
|
+
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
246
|
+
// Dashboard drain-and-restart (#563) must not tear down injection: the replacement
|
|
247
|
+
// process expects Codex/Grok/env fences to still be in place.
|
|
248
|
+
const recycling = isRecyclingForExit();
|
|
249
|
+
if (!recycling) {
|
|
250
|
+
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
251
|
+
}
|
|
252
|
+
removePid(process.pid);
|
|
253
|
+
removeRuntimePort(process.pid);
|
|
254
|
+
if (!recycling && !process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
255
|
+
try {
|
|
256
|
+
const restored = restoreNativeCodex();
|
|
257
|
+
if (!restored.success) {
|
|
258
|
+
cleanupSucceeded = false;
|
|
259
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
cleanupSucceeded = false;
|
|
263
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Same ownership rule as `ocx stop`: if the installed service belongs to another home, the
|
|
267
|
+
// Grok fence is shared state we must not remove — that service keeps running and would be
|
|
268
|
+
// left pointing nowhere. This guard also covers signal-driven exits, which is the path that
|
|
269
|
+
// would otherwise bypass handleStop's gate entirely.
|
|
270
|
+
if (!recycling && !process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
271
|
+
try { stripGrokConfig(); } catch { /* best-effort restore */ }
|
|
272
|
+
}
|
|
273
|
+
return cleanupSucceeded;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let shuttingDown = false;
|
|
277
|
+
let shutdownStartedAt = 0;
|
|
278
|
+
// Terminal Ctrl-C delivers SIGINT to the whole foreground group AND the launcher
|
|
279
|
+
// forwards its own — two signals land within milliseconds. Treat a duplicate inside
|
|
280
|
+
// this window as the same Ctrl-C (one graceful drain); a deliberate later press
|
|
281
|
+
// escalates to an immediate force-exit ("gradual kill").
|
|
282
|
+
const FORCE_AFTER_MS = 500;
|
|
283
|
+
const shutdown = () => {
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
if (shuttingDown) {
|
|
286
|
+
if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
|
|
287
|
+
console.log("\n⏹ Force shutdown (second signal).");
|
|
288
|
+
try { syncCleanup(); } catch { /* best-effort */ }
|
|
289
|
+
process.exit(130);
|
|
290
|
+
}
|
|
291
|
+
shuttingDown = true;
|
|
292
|
+
shutdownStartedAt = now;
|
|
293
|
+
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
294
|
+
void (async () => {
|
|
295
|
+
try {
|
|
296
|
+
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
297
|
+
} finally {
|
|
298
|
+
const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
299
|
+
process.exit(restored ? 0 : 1);
|
|
300
|
+
}
|
|
301
|
+
})();
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
process.on("SIGINT", shutdown);
|
|
305
|
+
process.on("SIGTERM", shutdown);
|
|
306
|
+
// The launcher (bin/ocx.mjs) forwards SIGHUP too (e.g. terminal close); handle it
|
|
307
|
+
// gracefully here so it drains + cleans up instead of a default immediate kill.
|
|
308
|
+
process.on("SIGHUP", shutdown);
|
|
309
|
+
process.on("exit", syncCleanup);
|
|
310
|
+
|
|
311
|
+
// System-wide env injection AFTER signal handlers are registered (crash safety:
|
|
312
|
+
// syncCleanup reverts even if injection itself or subsequent startup steps fail).
|
|
313
|
+
await injectSystemEnv(port, config).catch(() => {});
|
|
314
|
+
// Auto-install .zshrc hook (idempotent — skips if already present).
|
|
315
|
+
installShellHook();
|
|
316
|
+
|
|
317
|
+
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
318
|
+
await syncModelsToCodex(port).catch(() => {});
|
|
319
|
+
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
320
|
+
historyGuardian = startHistoryMigrationGuardian();
|
|
321
|
+
}
|
|
322
|
+
// Build Desktop 3P alias registry so inbound claude-opus-4-8-{code} aliases (and legacy claude-opus-4-{code}) decode correctly.
|
|
323
|
+
try {
|
|
324
|
+
const { fetchAllModels } = await import("../server/management-api");
|
|
325
|
+
const { visibleNativeSlugs, filterCatalogVisibleModels } = await import("../codex/catalog");
|
|
326
|
+
const models = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
327
|
+
buildDesktop3pRegistry(
|
|
328
|
+
[...visibleNativeSlugs(config)],
|
|
329
|
+
models.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
330
|
+
config.claudeCode?.desktopProfile,
|
|
331
|
+
);
|
|
332
|
+
} catch { /* best-effort — registry rebuilds on first /v1/models call */ }
|
|
333
|
+
// Grok Build auto-registration: additive fenced block in ~/.grok/config.toml so an installed
|
|
334
|
+
// grok CLI can pick opencodex-routed models without manual config. No-op when ~/.grok is
|
|
335
|
+
// absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
|
|
336
|
+
// Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
|
|
337
|
+
// failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
|
|
338
|
+
try {
|
|
339
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
340
|
+
const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
341
|
+
if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
342
|
+
else if (!r.ok) console.error(`⚠️ ${r.message}`);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// Best-effort: grok integration must never block startup. But swallowing the error
|
|
345
|
+
// silently is how a stale fence survives unnoticed — ~/.grok/config.toml keeps
|
|
346
|
+
// pointing at whatever port the LAST successful sync wrote, and if that listener is
|
|
347
|
+
// gone every grok turn retries against a refused connection with nothing in our log
|
|
348
|
+
// to explain it. Name the failure and the one command that repairs it.
|
|
349
|
+
console.error(`⚠️ ${grokSyncFailureMessage(err)}`);
|
|
350
|
+
}
|
|
351
|
+
if (options.block ?? true) {
|
|
352
|
+
setInterval(() => {}, 60_000);
|
|
353
|
+
await new Promise<void>(() => {});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function handleEnsure() {
|
|
358
|
+
if (!currentExternalCodexModelProvider()) reconcileJournal();
|
|
359
|
+
const config = loadConfig();
|
|
360
|
+
if (!codexAutoStartEnabled(config)) {
|
|
361
|
+
console.log("Codex autostart is disabled.");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const live = await findLiveProxy();
|
|
365
|
+
if (live) {
|
|
366
|
+
await syncModelsToCodex(live.port).catch(e => {
|
|
367
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
368
|
+
});
|
|
369
|
+
// Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
|
|
370
|
+
await injectSystemEnv(live.port, config).catch(() => {});
|
|
371
|
+
// Refresh the Grok Build fence too (same contract as start). live.hostname is the
|
|
372
|
+
// hostname the running proxy actually bound — config.hostname may have drifted.
|
|
373
|
+
try {
|
|
374
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
375
|
+
const g = await syncGrokConfig(live.port, config, live.hostname ? { hostname: live.hostname } : {});
|
|
376
|
+
if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
377
|
+
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
378
|
+
} catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
|
|
379
|
+
console.log(`✅ Proxy running on port ${live.port}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const pinPort = config.port ?? 10100;
|
|
384
|
+
const child = spawn(process.execPath, startArgv(pinPort > 0 ? pinPort : undefined), {
|
|
385
|
+
detached: true,
|
|
386
|
+
stdio: "ignore",
|
|
387
|
+
windowsHide: true,
|
|
388
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
389
|
+
});
|
|
390
|
+
child.unref();
|
|
391
|
+
|
|
392
|
+
const port = (await waitForProxy())?.port;
|
|
393
|
+
if (!port) {
|
|
394
|
+
console.error("❌ Proxy did not become healthy after starting.");
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
// Deterministic fence guarantee: the spawned child injects late in its own startup, but
|
|
398
|
+
// this parent returns as soon as /healthz responds — inject here too (idempotent block
|
|
399
|
+
// replace) so `ocx ensure` never returns without the Grok fence in place.
|
|
400
|
+
try {
|
|
401
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
402
|
+
const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
403
|
+
if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
404
|
+
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
405
|
+
} catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
|
|
406
|
+
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
407
|
+
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
408
|
+
await syncModelsToCodex(port).catch(e => {
|
|
409
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
410
|
+
});
|
|
411
|
+
console.log(`✅ Proxy running on port ${port}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Fixed tray action: start the proxy without depending on codexAutoStart. */
|
|
415
|
+
async function handleTrayProxyStart(): Promise<void> {
|
|
416
|
+
const ok = await runTrayProxyStart({
|
|
417
|
+
findLive: findLiveProxy,
|
|
418
|
+
diagnoseService: () => {
|
|
419
|
+
const service = diagnoseService();
|
|
420
|
+
return { installed: service.installed, startable: serviceStartableFromTray(service), summary: service.summary };
|
|
421
|
+
},
|
|
422
|
+
startService: () => serviceCommand("start"),
|
|
423
|
+
startDirect: () => {
|
|
424
|
+
const config = loadConfig();
|
|
425
|
+
const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100;
|
|
426
|
+
const child = spawn(process.execPath, startArgv(port), {
|
|
427
|
+
detached: true,
|
|
428
|
+
stdio: "ignore",
|
|
429
|
+
windowsHide: true,
|
|
430
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
431
|
+
});
|
|
432
|
+
child.unref();
|
|
433
|
+
},
|
|
434
|
+
waitForProxy,
|
|
435
|
+
info: message => console.log(message),
|
|
436
|
+
error: message => console.error(message),
|
|
437
|
+
});
|
|
438
|
+
if (!ok) process.exitCode = 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function handleTrayProxyRestart(): Promise<void> {
|
|
442
|
+
const ok = await runTrayProxyRestart({
|
|
443
|
+
stop: async () => {
|
|
444
|
+
await handleStop();
|
|
445
|
+
return !process.exitCode || process.exitCode === 0;
|
|
446
|
+
},
|
|
447
|
+
start: async () => {
|
|
448
|
+
await handleTrayProxyStart();
|
|
449
|
+
return !process.exitCode || process.exitCode === 0;
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
if (!ok) process.exitCode = 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function handleStop() {
|
|
456
|
+
let stopFailed = false;
|
|
457
|
+
let stoppedService = false;
|
|
458
|
+
// An ownership mismatch means the service manager was never even contacted: the installed
|
|
459
|
+
// service is still live and will respawn the proxy. Tearing down SHARED state in that
|
|
460
|
+
// situation (native Codex config, the Grok fence) removes config out from under a running
|
|
461
|
+
// service — the exact failure this flag prevents. A plain stop failure is different: we
|
|
462
|
+
// tried, so local teardown still proceeds.
|
|
463
|
+
let ownershipBlocked = false;
|
|
464
|
+
try {
|
|
465
|
+
stoppedService = stopServiceIfInstalled();
|
|
466
|
+
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
if (isServiceOwnershipError(err)) {
|
|
469
|
+
ownershipBlocked = true;
|
|
470
|
+
stopFailed = true;
|
|
471
|
+
console.error(`❌ ${err.message}`);
|
|
472
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the installed service is still running.");
|
|
473
|
+
} else {
|
|
474
|
+
console.error(`⚠️ Service manager stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const pid = readPid();
|
|
479
|
+
if (pid) {
|
|
480
|
+
try {
|
|
481
|
+
// Graceful-first (management-API drain) — on Windows this is the only path where
|
|
482
|
+
// the proxy's shutdown handlers actually run; taskkill /F is the fallback inside.
|
|
483
|
+
await stopProxy(pid);
|
|
484
|
+
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
485
|
+
removePid(pid);
|
|
486
|
+
removeRuntimePort(pid);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
stopFailed = true;
|
|
489
|
+
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
490
|
+
// stopProxy throws with the reason — an ownership refusal (409) carries the
|
|
491
|
+
// remediation ("run the stop from that home"). Swallowing it leaves the operator
|
|
492
|
+
// with a bare failure and a manual `kill` as the obvious next move, which is the
|
|
493
|
+
// exact teardown the refusal exists to prevent.
|
|
494
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
495
|
+
if (detail) console.error(` ${detail}`);
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
// Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
|
|
499
|
+
// can write fresh records mid-probe, and the purge below must never delete those.
|
|
500
|
+
const stalePidValue = readPidFileValue();
|
|
501
|
+
const staleRuntimePid = readRuntimePort()?.pid ?? null;
|
|
502
|
+
// Orphan recovery: a live proxy can outlive its pid file (crash, manual delete,
|
|
503
|
+
// corrupt file). Identity-checked liveness still finds it via the runtime record.
|
|
504
|
+
const live = await findLiveProxy();
|
|
505
|
+
if (live?.pid) {
|
|
506
|
+
try {
|
|
507
|
+
await stopProxy(live.pid);
|
|
508
|
+
console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
stopFailed = true;
|
|
511
|
+
console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
|
|
512
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
513
|
+
if (detail) console.error(` ${detail}`);
|
|
514
|
+
}
|
|
515
|
+
} else if (!stoppedService) {
|
|
516
|
+
console.log("No running proxy found.");
|
|
517
|
+
}
|
|
518
|
+
if (!stopFailed) {
|
|
519
|
+
// `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
|
|
520
|
+
// not ours — stale by definition. Purge (guarded by the snapshot) so `ocx update`'s
|
|
521
|
+
// stop gate can't wedge on it.
|
|
522
|
+
removePidIfValueIs(stalePidValue);
|
|
523
|
+
removeRuntimePortIfPidIs(staleRuntimePid);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!ownershipBlocked) {
|
|
527
|
+
const r = restoreNativeCodex();
|
|
528
|
+
if (r.success) console.log(`↩️ ${r.message}`);
|
|
529
|
+
else {
|
|
530
|
+
stopFailed = true;
|
|
531
|
+
console.error(`⚠️ ${r.message}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
|
|
535
|
+
// user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
|
|
536
|
+
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
537
|
+
if (!ownershipBlocked) {
|
|
538
|
+
// Same safety net for the Grok Build managed block (marker-owned, idempotent).
|
|
539
|
+
try {
|
|
540
|
+
const g = stripGrokConfig();
|
|
541
|
+
if (g.changed) console.log(`↩️ ${g.message}`);
|
|
542
|
+
// A refused strip (e.g. orphaned marker) leaves the fence pointing at a dead proxy —
|
|
543
|
+
// reporting success there hides a broken end state.
|
|
544
|
+
else if (!g.ok) { stopFailed = true; console.error(`⚠️ ${g.message}`); }
|
|
545
|
+
} catch { /* best-effort */ }
|
|
546
|
+
}
|
|
547
|
+
// Set the code rather than exiting inline: `restart` and the tray coordinator call this
|
|
548
|
+
// function and need it to RETURN so they can decide what to do next.
|
|
549
|
+
if (stopFailed) process.exitCode = 1;
|
|
550
|
+
return !stopFailed;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function handleUninstall() {
|
|
554
|
+
const failures: string[] = [];
|
|
555
|
+
|
|
556
|
+
const runStep = async (label: string, step: () => void | boolean | Promise<void | boolean>) => {
|
|
557
|
+
try {
|
|
558
|
+
const changed = await step();
|
|
559
|
+
if (changed === false) console.log(`- ${label}: not installed`);
|
|
560
|
+
else console.log(`✅ ${label}`);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
failures.push(label);
|
|
563
|
+
console.error(`⚠️ ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
await runStep("service stopped", () => stopServiceIfInstalled());
|
|
568
|
+
|
|
569
|
+
await runStep("proxy stopped", async () => {
|
|
570
|
+
const pid = readPid();
|
|
571
|
+
if (!pid) return false;
|
|
572
|
+
await stopProxy(pid);
|
|
573
|
+
removePid(pid);
|
|
574
|
+
removeRuntimePort(pid);
|
|
575
|
+
return true;
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
await runStep("service removed", () => uninstallServiceIfInstalled());
|
|
579
|
+
|
|
580
|
+
if (process.platform === "win32") {
|
|
581
|
+
await runStep("Windows tray removed", async () => {
|
|
582
|
+
const { getWindowsTrayStatus, uninstallWindowsTray } = await import("../tray/windows");
|
|
583
|
+
const tray = getWindowsTrayStatus();
|
|
584
|
+
if (!tray.installed && !tray.stale && !tray.running) return false;
|
|
585
|
+
uninstallWindowsTray();
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
await runStep("native Codex restored", () => {
|
|
590
|
+
const r = restoreNativeCodex();
|
|
591
|
+
if (!r.success) throw new Error(r.message);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
await runStep("Grok Build config restored", () => {
|
|
595
|
+
const r = stripGrokConfig();
|
|
596
|
+
if (!r.ok) throw new Error(r.message);
|
|
597
|
+
return r.changed;
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
await runStep("system env vars reverted", () => {
|
|
601
|
+
const r = revertSystemEnv();
|
|
602
|
+
if (!r.reverted && r.reason !== "no tracking file" && r.reason !== "not macOS") throw new Error(r.reason ?? "revert failed");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
await runStep("shell hook removed", () => {
|
|
606
|
+
const r = uninstallShellHook();
|
|
607
|
+
if (!r.removed && r.reason !== "not installed" && r.reason !== "not macOS") throw new Error(r.reason ?? "remove failed");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const { uninstallCodexShim } = await import("../codex/shim");
|
|
612
|
+
const r = uninstallCodexShim();
|
|
613
|
+
console.log(r.removed ? "✅ Codex autostart shim removed" : "- Codex autostart shim removed: not installed");
|
|
614
|
+
} catch (err) {
|
|
615
|
+
failures.push("Codex autostart shim removed");
|
|
616
|
+
console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (failures.length === 0) {
|
|
620
|
+
await runStep("opencodex config removed", () => {
|
|
621
|
+
const result = removeOwnedConfigState(getConfigDir());
|
|
622
|
+
if (result.status === "absent") return false;
|
|
623
|
+
if (result.status === "removed") return true;
|
|
624
|
+
const residual = result.residualPaths.length > 0
|
|
625
|
+
? ` Residual path(s): ${result.residualPaths.join(", ")}`
|
|
626
|
+
: "";
|
|
627
|
+
throw new Error(`${result.status} uninstall: ${result.reason ?? "config state was not removed"}.${residual}`);
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
console.error("Leaving opencodex config/backups in place so the failed restore step can be retried.");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (failures.length > 0) {
|
|
634
|
+
console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @iislee/opencodex");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function handleStatus() {
|
|
641
|
+
const statusArgs = args.slice(1);
|
|
642
|
+
const wantsJson = statusArgs.length === 1 && statusArgs[0] === "--json";
|
|
643
|
+
if (statusArgs.length > 1 || (statusArgs.length === 1 && !wantsJson)) {
|
|
644
|
+
console.error("Usage: ocx status [--json]");
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const status = await collectStatus();
|
|
649
|
+
if (wantsJson) {
|
|
650
|
+
console.log(JSON.stringify(status.json, null, 2));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (status.json.proxy.pid || status.json.proxy.health.ok) {
|
|
655
|
+
console.log(`✅ Proxy: ${status.proxyLabel}`);
|
|
656
|
+
} else {
|
|
657
|
+
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
658
|
+
}
|
|
659
|
+
console.log(` Health: ${status.healthLabel}`);
|
|
660
|
+
if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
|
|
661
|
+
console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
|
|
662
|
+
console.log(" Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
|
663
|
+
}
|
|
664
|
+
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
665
|
+
console.log(` Config: ${status.json.paths.config}`);
|
|
666
|
+
console.log(` PID file: ${status.json.paths.pid}`);
|
|
667
|
+
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
668
|
+
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
669
|
+
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
670
|
+
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
671
|
+
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
672
|
+
console.log(` Service: ${status.json.service.summary}`);
|
|
673
|
+
console.log(` ${status.json.codexShim.summary}`);
|
|
674
|
+
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
|
675
|
+
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
|
|
676
|
+
console.log(` Codex source: ${status.json.codexRuntime.source}`);
|
|
677
|
+
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`);
|
|
678
|
+
if (status.json.codexHome.warning) {
|
|
679
|
+
console.log(` ⚠️ ${status.json.codexHome.warning}`);
|
|
680
|
+
console.log(` Action: ${status.json.codexHome.action}`);
|
|
681
|
+
}
|
|
682
|
+
console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`);
|
|
683
|
+
if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) {
|
|
684
|
+
console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`);
|
|
685
|
+
}
|
|
686
|
+
if (status.json.codexRuntime.warning) {
|
|
687
|
+
console.log(` ⚠️ ${status.json.codexRuntime.warning}`);
|
|
688
|
+
}
|
|
689
|
+
if (status.json.codexPlugins.applicable) {
|
|
690
|
+
const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
|
|
691
|
+
console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
|
|
692
|
+
if (status.json.codexPlugins.suggestedRepair) {
|
|
693
|
+
console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
697
|
+
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
698
|
+
console.log(` OAuth logins:`);
|
|
699
|
+
for (const e of oauthLoginSummary()) {
|
|
700
|
+
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
701
|
+
}
|
|
702
|
+
const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
|
|
703
|
+
if (oauthHealthBlock) {
|
|
704
|
+
for (const line of oauthHealthBlock.split("\n")) {
|
|
705
|
+
console.log(` ${line}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function handleRecoverHistory() {
|
|
711
|
+
if (args[1] !== "--legacy-openai") {
|
|
712
|
+
console.error("Usage: ocx recover-history --legacy-openai");
|
|
713
|
+
console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
const r = restoreLegacyOpenaiHistory();
|
|
717
|
+
if (r.failed) {
|
|
718
|
+
console.error(
|
|
719
|
+
"⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
|
|
720
|
+
);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
switch (command) {
|
|
727
|
+
case "init":
|
|
728
|
+
case "setup": {
|
|
729
|
+
const { runInit } = await import("./init");
|
|
730
|
+
await runInit();
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case "start":
|
|
734
|
+
await handleStart();
|
|
735
|
+
break;
|
|
736
|
+
case "stop": {
|
|
737
|
+
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
|
|
738
|
+
// re-start the proxy immediately, so warning there would contradict the next line.
|
|
739
|
+
if (await handleStop()) {
|
|
740
|
+
console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
|
|
741
|
+
}
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case "restore":
|
|
745
|
+
case "eject": {
|
|
746
|
+
if (args[1] === "back") {
|
|
747
|
+
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
748
|
+
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
749
|
+
// this only re-runs the same inject (config + catalog + history) `ocx start` does.
|
|
750
|
+
const live = await findLiveProxy();
|
|
751
|
+
if (!live) {
|
|
752
|
+
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
const synced = await syncModelsToCodex(live.port);
|
|
756
|
+
if (!synced.ok) {
|
|
757
|
+
process.exitCode = 1;
|
|
758
|
+
console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
const target = collectOrcaCodexHomeDiagnostic();
|
|
762
|
+
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
let r: { success: boolean; message: string };
|
|
766
|
+
try {
|
|
767
|
+
r = restoreNativeCodex();
|
|
768
|
+
} catch (err) {
|
|
769
|
+
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
770
|
+
}
|
|
771
|
+
if (r.success) console.log(`✅ ${r.message}`);
|
|
772
|
+
else {
|
|
773
|
+
console.error(`⚠️ ${r.message}`);
|
|
774
|
+
process.exitCode = 1;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const g = stripGrokConfig();
|
|
778
|
+
if (g.changed) console.log(`✅ ${g.message}`);
|
|
779
|
+
else if (!g.ok) {
|
|
780
|
+
console.error(`⚠️ ${g.message}`);
|
|
781
|
+
process.exitCode = 1;
|
|
782
|
+
}
|
|
783
|
+
} catch { /* best-effort */ }
|
|
784
|
+
if (r.success) {
|
|
785
|
+
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
786
|
+
} else {
|
|
787
|
+
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case "recover-history":
|
|
792
|
+
handleRecoverHistory();
|
|
793
|
+
break;
|
|
794
|
+
case "uninstall":
|
|
795
|
+
case "remove":
|
|
796
|
+
await handleUninstall();
|
|
797
|
+
break;
|
|
798
|
+
case "status":
|
|
799
|
+
await handleStatus();
|
|
800
|
+
break;
|
|
801
|
+
case "doctor": {
|
|
802
|
+
const { runDoctor } = await import("./doctor");
|
|
803
|
+
await runDoctor(args.slice(1));
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "debug": {
|
|
807
|
+
const { handleDebugCommand } = await import("./debug");
|
|
808
|
+
await handleDebugCommand(args.slice(1));
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case "ensure":
|
|
812
|
+
await handleEnsure();
|
|
813
|
+
break;
|
|
814
|
+
case "login": {
|
|
815
|
+
const { handleLogin } = await import("../oauth/login-cli");
|
|
816
|
+
await handleLogin(args[1]);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case "logout": {
|
|
820
|
+
const { removeCredential } = await import("../oauth/store");
|
|
821
|
+
const name = (args[1] ?? "").trim().toLowerCase();
|
|
822
|
+
await removeCredential(name);
|
|
823
|
+
console.log(`Logged out of ${name || "(none)"}.`);
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
case "sync": {
|
|
827
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
828
|
+
const synced = await syncModelsToCodex((await findLiveProxy())?.port);
|
|
829
|
+
if (!synced.ok) {
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
|
|
832
|
+
}
|
|
833
|
+
// Only warn/restart when a catalog or models_cache write actually happened. This is
|
|
834
|
+
// deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
|
|
835
|
+
// so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
|
|
836
|
+
// exactly when a long-lived app-server is holding the stale list.
|
|
837
|
+
if (synced.catalogWritten || synced.cacheSynced) {
|
|
838
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
839
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case "v2": {
|
|
844
|
+
const { cmdV2 } = await import("./v2");
|
|
845
|
+
process.exitCode = await cmdV2(args.slice(1), {}, async () => (await findLiveProxy())?.port);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
case "sync-cache": {
|
|
849
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
850
|
+
const { invalidateCodexModelsCache } = await import("../codex/catalog");
|
|
851
|
+
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
852
|
+
if (invalidateCodexModelsCache()) {
|
|
853
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
854
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case "gui": {
|
|
859
|
+
const cfg = await import("../config");
|
|
860
|
+
const config = cfg.loadConfig();
|
|
861
|
+
// Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
|
|
862
|
+
// proxy and waits until the spawned one actually answers before opening the browser.
|
|
863
|
+
let live = await findLiveProxy();
|
|
864
|
+
if (!live) {
|
|
865
|
+
console.log("Proxy not running. Starting...");
|
|
866
|
+
const child = spawn(process.execPath, startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined), {
|
|
867
|
+
detached: true,
|
|
868
|
+
stdio: "ignore",
|
|
869
|
+
windowsHide: true,
|
|
870
|
+
env: process.env,
|
|
871
|
+
});
|
|
872
|
+
child.unref();
|
|
873
|
+
live = await waitForProxy();
|
|
874
|
+
if (!live) {
|
|
875
|
+
console.error("❌ Proxy did not become healthy after starting. Not opening the GUI.");
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Open the host the proxy actually binds — `localhost` only answers for
|
|
880
|
+
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
881
|
+
const guiHost = probeHostname(live?.hostname ?? config.hostname);
|
|
882
|
+
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
883
|
+
console.log(`Opening ${guiUrl}`);
|
|
884
|
+
const { openUrl } = await import("../lib/open-url");
|
|
885
|
+
openUrl(guiUrl);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
case "service":
|
|
889
|
+
await serviceCommand(...args.slice(1));
|
|
890
|
+
break;
|
|
891
|
+
case "tray": {
|
|
892
|
+
const { windowsTrayCommand } = await import("../tray/windows");
|
|
893
|
+
await windowsTrayCommand(args.slice(1));
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case "codex-shim": {
|
|
897
|
+
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
898
|
+
switch (args[1]) {
|
|
899
|
+
case "install": {
|
|
900
|
+
const r = installCodexShim();
|
|
901
|
+
console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
case "status":
|
|
905
|
+
console.log(codexShimStatus());
|
|
906
|
+
break;
|
|
907
|
+
case "uninstall":
|
|
908
|
+
case "remove": {
|
|
909
|
+
const r = uninstallCodexShim();
|
|
910
|
+
console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
default:
|
|
914
|
+
console.error("Usage: ocx codex-shim <install|status|uninstall|remove>");
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "update": {
|
|
920
|
+
// `ocx update --help` must print usage and exit WITHOUT side effects — running the
|
|
921
|
+
// real self-update stops the proxy and drops in-flight routed streams (issue #168).
|
|
922
|
+
if (hasHelpFlag(args.slice(1))) {
|
|
923
|
+
printSubcommandUsage("update");
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
const { runUpdate } = await import("../update");
|
|
927
|
+
await runUpdate();
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "__refresh-version": {
|
|
931
|
+
// Hidden, detached helper spawned by the update prompt to refresh the
|
|
932
|
+
// cached latest version without blocking the foreground start. Not in help.
|
|
933
|
+
const { refreshVersionCache } = await import("../update/notify");
|
|
934
|
+
const channel = args[1] === "preview" ? "preview" : "latest";
|
|
935
|
+
await refreshVersionCache(channel);
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
case "__tray-start":
|
|
939
|
+
case "__tray-restart":
|
|
940
|
+
case "__startup-health":
|
|
941
|
+
await dispatchInternalCliCommand(command as InternalCliCommand, {
|
|
942
|
+
trayStart: handleTrayProxyStart,
|
|
943
|
+
trayRestart: handleTrayProxyRestart,
|
|
944
|
+
startupHealth: async () => {
|
|
945
|
+
const { collectStartupHealth } = await import("../codex/autostart-health");
|
|
946
|
+
console.log(JSON.stringify(collectStartupHealth(loadConfig())));
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
break;
|
|
950
|
+
case "__tray-host": {
|
|
951
|
+
const { runWindowsTrayHost } = await import("../tray/windows");
|
|
952
|
+
await runWindowsTrayHost();
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
case "__gui-update-worker": {
|
|
956
|
+
const jobId = args[1];
|
|
957
|
+
if (!jobId) process.exit(1);
|
|
958
|
+
const channel = normalizeUpdateChannel(args[2]);
|
|
959
|
+
await runGuiUpdateWorker(jobId, channel, args[3] === "restart");
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
case "restart": {
|
|
963
|
+
// A failed stop must not be followed by a re-inject: with a foreign service still running
|
|
964
|
+
// (ownership mismatch) we would rewrite shared config we just declined to touch.
|
|
965
|
+
if (await handleStop()) await handleEnsure();
|
|
966
|
+
else console.error("↩️ Restart aborted: the proxy was not stopped cleanly.");
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case "health": {
|
|
970
|
+
const healthArgs = args.slice(1);
|
|
971
|
+
const wantsHealthJson = healthArgs.includes("--json");
|
|
972
|
+
const live = await findLiveProxy();
|
|
973
|
+
if (wantsHealthJson) {
|
|
974
|
+
console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
|
|
975
|
+
} else {
|
|
976
|
+
console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
|
|
977
|
+
}
|
|
978
|
+
process.exit(live ? 0 : 1);
|
|
979
|
+
}
|
|
980
|
+
case "provider": {
|
|
981
|
+
const { handleProviderCommand } = await import("./provider");
|
|
982
|
+
await handleProviderCommand(args.slice(1));
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
case "account": {
|
|
986
|
+
const { cmdAccount } = await import("./account");
|
|
987
|
+
process.exitCode = await cmdAccount(args.slice(1));
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "models":
|
|
991
|
+
case "model": {
|
|
992
|
+
const { handleModels } = await import("./models");
|
|
993
|
+
await handleModels(args.slice(1));
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
case "combo": {
|
|
997
|
+
const { handleComboCommand } = await import("./combo");
|
|
998
|
+
process.exitCode = await handleComboCommand(args.slice(1));
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
case "route": {
|
|
1002
|
+
if (args[1] !== "combo") {
|
|
1003
|
+
console.error("Usage: ocx route combo <subcommand>");
|
|
1004
|
+
process.exitCode = 2;
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
const { handleComboCommand } = await import("./combo");
|
|
1008
|
+
process.exitCode = await handleComboCommand(args.slice(2));
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "agent": {
|
|
1012
|
+
const { handleAgentCommand } = await import("./agent");
|
|
1013
|
+
process.exitCode = await handleAgentCommand(args.slice(1));
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case "observe": {
|
|
1017
|
+
const { handleObserveCommand } = await import("./observe");
|
|
1018
|
+
process.exitCode = await handleObserveCommand(args.slice(1));
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
case "logs":
|
|
1022
|
+
case "usage":
|
|
1023
|
+
case "storage":
|
|
1024
|
+
case "memory": {
|
|
1025
|
+
const { handleObserveCommand } = await import("./observe");
|
|
1026
|
+
process.exitCode = await handleObserveCommand([command, ...args.slice(1)]);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
case "access": {
|
|
1030
|
+
const { handleAccessCommand } = await import("./access");
|
|
1031
|
+
process.exitCode = await handleAccessCommand(args.slice(1));
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
case "api-key": {
|
|
1035
|
+
const { handleAccessCommand } = await import("./access");
|
|
1036
|
+
process.exitCode = await handleAccessCommand(["key", ...args.slice(1)]);
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
case "export": {
|
|
1040
|
+
const { handleExportCommand } = await import("./export-command");
|
|
1041
|
+
process.exitCode = await handleExportCommand(args.slice(1));
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case "grok": {
|
|
1045
|
+
const { handleGrokCommand } = await import("./integrations");
|
|
1046
|
+
process.exitCode = await handleGrokCommand(args.slice(1));
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
case "integration": {
|
|
1050
|
+
const integration = args[1];
|
|
1051
|
+
if (integration === "grok") {
|
|
1052
|
+
const { handleGrokCommand } = await import("./integrations");
|
|
1053
|
+
process.exitCode = await handleGrokCommand(args.slice(2));
|
|
1054
|
+
} else if (integration === "claude") {
|
|
1055
|
+
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
1056
|
+
process.exitCode = await handleClaudeConfigCommand(args.slice(2));
|
|
1057
|
+
} else {
|
|
1058
|
+
console.error("Usage: ocx integration <claude|grok> <subcommand>");
|
|
1059
|
+
process.exitCode = 2;
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
case "system": {
|
|
1064
|
+
const { handleSystemCommand } = await import("./system-command");
|
|
1065
|
+
process.exitCode = await handleSystemCommand(args.slice(1));
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
case "config": {
|
|
1069
|
+
const { handleConfigCommand } = await import("./config-command");
|
|
1070
|
+
process.exitCode = await handleConfigCommand(args.slice(1));
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "sync-cloud": {
|
|
1074
|
+
const { handleSyncCloudCommand } = await import("./sync-cloud");
|
|
1075
|
+
process.exitCode = await handleSyncCloudCommand(args.slice(1));
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "claude": {
|
|
1079
|
+
const { cmdClaude } = await import("./claude");
|
|
1080
|
+
// "ocx claude desktop" → write Desktop 3P config
|
|
1081
|
+
if (args[1] === "desktop") {
|
|
1082
|
+
const { handleClaudeDesktopCommand } = await import("./claude-desktop");
|
|
1083
|
+
const exitCode = await handleClaudeDesktopCommand(args.slice(2));
|
|
1084
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
if (args[1] === "config") {
|
|
1088
|
+
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
1089
|
+
process.exitCode = await handleClaudeConfigCommand(args.slice(2));
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
process.exit(await cmdClaude(args.slice(1)));
|
|
1093
|
+
}
|
|
1094
|
+
case "opencode": {
|
|
1095
|
+
const { cmdOpencode } = await import("./opencode");
|
|
1096
|
+
process.exit(await cmdOpencode(args.slice(1)));
|
|
1097
|
+
}
|
|
1098
|
+
case "pi": {
|
|
1099
|
+
const { handlePiCommand } = await import("./pi");
|
|
1100
|
+
process.exitCode = await handlePiCommand(args.slice(1));
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case "help":
|
|
1104
|
+
case "--help":
|
|
1105
|
+
case "-h":
|
|
1106
|
+
case undefined:
|
|
1107
|
+
printUsage();
|
|
1108
|
+
break;
|
|
1109
|
+
default:
|
|
1110
|
+
console.error(`Unknown command: ${command}`);
|
|
1111
|
+
printUsage();
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|