@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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx claude [claude args...]` — launch Claude Code wired to the local proxy.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `ccr code` UX (devlog/260711_claude_inbound/020, 003 E1/E2/E5/G1):
|
|
5
|
+
* ensures the proxy is running, injects the Anthropic env slots, then execs the
|
|
6
|
+
* `claude` CLI with stdio inherited. User-exported env wins except when a stale
|
|
7
|
+
* loopback opencodex base URL points at a different proxy port.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { loadConfig } from "../config";
|
|
11
|
+
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
12
|
+
import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
13
|
+
import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
|
|
14
|
+
import { commandInvocation } from "../lib/win-exec";
|
|
15
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
16
|
+
import type { OcxConfig } from "../types";
|
|
17
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
18
|
+
import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
|
|
19
|
+
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
20
|
+
|
|
21
|
+
export interface ClaudeLaunchEnv {
|
|
22
|
+
[key: string]: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Injectable IO for tests. `env` is deliberately NOT injectable: it is bound to the
|
|
27
|
+
* launch base so detection and the spawned process can never disagree (audit R3-3).
|
|
28
|
+
*/
|
|
29
|
+
export type ClaudeEnvDeps = { authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens"> };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure env assembly (unit-tested): never sets ANTHROPIC_API_KEY (setting both
|
|
33
|
+
* token vars triggers Claude Code's auth-conflict warning, 003 E1), and never
|
|
34
|
+
* overrides variables the user already exported, apart from stale loopback
|
|
35
|
+
* ANTHROPIC_BASE_URL values owned by a previous opencodex launch.
|
|
36
|
+
*/
|
|
37
|
+
export function buildClaudeEnv(
|
|
38
|
+
config: OcxConfig,
|
|
39
|
+
port: number,
|
|
40
|
+
base: ClaudeLaunchEnv,
|
|
41
|
+
contextWindows: Record<string, number> = {},
|
|
42
|
+
deps: ClaudeEnvDeps = {},
|
|
43
|
+
): ClaudeLaunchEnv {
|
|
44
|
+
const env: ClaudeLaunchEnv = { ...base };
|
|
45
|
+
// Step 1 — strip OUR OWN dummy from the inherited environment before anything reads
|
|
46
|
+
// or writes the token slot. setDefault below preserves any non-empty value, so a
|
|
47
|
+
// stale marker left in place would suppress the admission key and then be removed,
|
|
48
|
+
// leaving the child with no token at all (audit R2-1). It is opencodex state, never
|
|
49
|
+
// user auth, so dropping it unconditionally is safe.
|
|
50
|
+
if (env.ANTHROPIC_AUTH_TOKEN === PROXY_MARKER) delete env.ANTHROPIC_AUTH_TOKEN;
|
|
51
|
+
// Step 1b — drop Anthropic credentials that the bundled Bun runtime synthesized from a
|
|
52
|
+
// project `.env`/`.env.local` (issue #701). Claude Code disables claude.ai connectors the
|
|
53
|
+
// moment either token slot is populated, so an ambient project file silently moved a
|
|
54
|
+
// subscriber onto API billing while their OAuth login stayed healthy. The npm launcher
|
|
55
|
+
// runs under Node, which does NOT auto-load dotenv, so it records the slots that existed
|
|
56
|
+
// before Bun started; anything populated now but absent then came from the working
|
|
57
|
+
// directory, not from the user. A genuine shell export is still honored, which keeps
|
|
58
|
+
// auto-mode API-key auth working. An ABSENT marker means provenance is unknowable
|
|
59
|
+
// (a direct `bun src/cli/index.ts` run, a test, or an older launcher), and then we
|
|
60
|
+
// change nothing rather than guess — an EMPTY marker is different: the launcher ran
|
|
61
|
+
// and saw no pre-existing slots.
|
|
62
|
+
const preBunSlots = base.OCX_PRE_BUN_ANTHROPIC_ENV;
|
|
63
|
+
if (preBunSlots !== undefined) {
|
|
64
|
+
const exported = new Set(preBunSlots.split(",").filter(name => name.length > 0));
|
|
65
|
+
for (const name of ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"] as const) {
|
|
66
|
+
const value = env[name];
|
|
67
|
+
if (value !== undefined && value !== "" && !exported.has(name)) delete env[name];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Never forward the seam itself to Claude Code.
|
|
71
|
+
delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
|
|
72
|
+
const setDefault = (name: string, value: string | undefined) => {
|
|
73
|
+
if (value === undefined || value.length === 0) return;
|
|
74
|
+
if (env[name] !== undefined && env[name] !== "") return; // user wins
|
|
75
|
+
env[name] = value;
|
|
76
|
+
};
|
|
77
|
+
setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
|
|
78
|
+
const existingBaseUrl = env.ANTHROPIC_BASE_URL;
|
|
79
|
+
if (existingBaseUrl) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new URL(existingBaseUrl);
|
|
82
|
+
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
83
|
+
if (isLoopback && parsed.port !== "" && Number(parsed.port) !== port) {
|
|
84
|
+
const replacement = `http://127.0.0.1:${port}`;
|
|
85
|
+
console.error(`⚠ Replacing stale opencodex ANTHROPIC_BASE_URL ${existingBaseUrl} with ${replacement}.`);
|
|
86
|
+
env.ANTHROPIC_BASE_URL = replacement;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Preserve user-provided values that are not parseable URLs.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Subscription-preserving default (teamclaude --no-mitm / Vercel gateway pattern):
|
|
93
|
+
// setting ANTHROPIC_AUTH_TOKEN/API_KEY disables claude.ai connectors and overrides
|
|
94
|
+
// the user's Claude login. Only inject a token when the proxy actually requires an
|
|
95
|
+
// admission key; otherwise Claude Code keeps its own OAuth and sends it to us —
|
|
96
|
+
// native claude models then pass through verbatim (see server/claude-messages.ts).
|
|
97
|
+
if ((config.apiKeys?.length ?? 0) > 0) {
|
|
98
|
+
setDefault("ANTHROPIC_AUTH_TOKEN", config.apiKeys![0].key);
|
|
99
|
+
}
|
|
100
|
+
// Detection reads the SANITIZED launch env — the exact object spawned below — so the
|
|
101
|
+
// resolver and the spawned process cannot disagree. It deliberately does NOT read the
|
|
102
|
+
// raw base: the provenance strip above already removed dotenv-only credentials, and
|
|
103
|
+
// letting a value the child never receives decide the marker left an auto-mode user
|
|
104
|
+
// with neither the credential NOR the proxy marker (#701 audit round 2). Injected deps
|
|
105
|
+
// are spread FIRST and `env` bound LAST, and the injection type excludes `env`, so a
|
|
106
|
+
// test fake cannot break that. `ownTokens` is bound last for the same reason: it is
|
|
107
|
+
// config-derived, and a fake that replaced it could make our own admission key look
|
|
108
|
+
// like user auth.
|
|
109
|
+
const resolved = resolveClaudeAuthMode(config, detectClaudeAuth({
|
|
110
|
+
...defaultAuthDetectDeps(env as NodeJS.ProcessEnv),
|
|
111
|
+
...(deps.authDetect ?? {}),
|
|
112
|
+
env: () => env as NodeJS.ProcessEnv,
|
|
113
|
+
ownTokens: ownAdmissionTokens(config),
|
|
114
|
+
}));
|
|
115
|
+
if (!env.ANTHROPIC_AUTH_TOKEN && resolved.markerMode === "proxy") {
|
|
116
|
+
env.ANTHROPIC_AUTH_TOKEN = PROXY_MARKER;
|
|
117
|
+
}
|
|
118
|
+
if (resolved.origin === "auto-unknown") {
|
|
119
|
+
console.error("⚠ Claude 인증을 확인하지 못했습니다 — 구독 방식으로 진행합니다. GUI에서 인증 모드를 직접 지정하면 이 판단을 덮어쓸 수 있습니다.");
|
|
120
|
+
}
|
|
121
|
+
// NOTE: do NOT set _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL here. While it enables
|
|
122
|
+
// Design/Remote Control, it DISABLES gateway model discovery (Claude Code's eligibility
|
|
123
|
+
// check returns false when isFirstPartyBaseUrl() is true). Model routing through the
|
|
124
|
+
// proxy is essential; Design/Remote Control are secondary features.
|
|
125
|
+
// Connectors still work because they check OAuth state ($o()), not base URL (Gd()).
|
|
126
|
+
// Native /model picker discovery ("From gateway", Claude Code >= 2.1.129).
|
|
127
|
+
setDefault("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1");
|
|
128
|
+
// Host-managed routing guard (devlog 260720_claude_authmode_persist/020): with
|
|
129
|
+
// this flag in the spawn env, Claude Code strips provider-managed vars
|
|
130
|
+
// (ANTHROPIC_BASE_URL/AUTH_TOKEN/API_KEY, model slots) from settings-sourced
|
|
131
|
+
// env (managedEnv.ts), so a leftover cc-switch/CCR ~/.claude/settings.json
|
|
132
|
+
// env block cannot silently hijack proxy routing away from opencodex.
|
|
133
|
+
// setDefault: an explicit user export (e.g. =0, isEnvTruthy-false) still wins.
|
|
134
|
+
// Intentional contract change: settings.env model slots are also stripped in
|
|
135
|
+
// ocx claude runs — use the top-level settings "model" field or opt out.
|
|
136
|
+
// Claude Code 2.1.206+ also treats this as a host-auth assertion. Injecting it
|
|
137
|
+
// without a host token makes a valid claude.ai subscription look logged out,
|
|
138
|
+
// so the guard is only safe when opencodex actually owns authentication.
|
|
139
|
+
if (env.ANTHROPIC_AUTH_TOKEN) {
|
|
140
|
+
setDefault("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", "1");
|
|
141
|
+
}
|
|
142
|
+
// Opt-in effort forcing (devlog 136 B6): opus-shaped aliases already carry
|
|
143
|
+
// output_config.effort, so this is OFF unless the user enables it in config.
|
|
144
|
+
if (config.claudeCode?.alwaysEnableEffort === true) {
|
|
145
|
+
setDefault("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
|
|
146
|
+
}
|
|
147
|
+
// Context-window override: the official pair — MAX_CONTEXT_TOKENS alone is ignored
|
|
148
|
+
// for recognized claude-shaped ids unless DISABLE_COMPACT=1 rides along (devlog 135).
|
|
149
|
+
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
150
|
+
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
151
|
+
setDefault("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
|
|
152
|
+
setDefault("DISABLE_COMPACT", "1");
|
|
153
|
+
}
|
|
154
|
+
// Auto-context (devlog 260712 020): min(believed window, env) inside the CLI means
|
|
155
|
+
// one global env acts as a per-model floor — [1m]-marked models compact here while
|
|
156
|
+
// unmarked (200k-accounted) models keep their default behavior. Inert when the
|
|
157
|
+
// legacy maxContextTokens pair above is set (resolveAutoContext handles that).
|
|
158
|
+
// A user-exported value drives the marking predicate too (audit 021 #2) so the
|
|
159
|
+
// [1m] marker and the compaction threshold can never separate.
|
|
160
|
+
const userAutoCompact = typeof base.CLAUDE_CODE_AUTO_COMPACT_WINDOW === "string" && base.CLAUDE_CODE_AUTO_COMPACT_WINDOW !== ""
|
|
161
|
+
? base.CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
162
|
+
: undefined;
|
|
163
|
+
const auto = resolveAutoContext(config.claudeCode, userAutoCompact);
|
|
164
|
+
if (auto.enabled) {
|
|
165
|
+
setDefault("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(auto.compactWindow));
|
|
166
|
+
}
|
|
167
|
+
// Model slots (devlog 260712 B2): default + four tier defaults + legacy small-fast,
|
|
168
|
+
// with automatic [1m] context-variant marking when the slot's target model has an
|
|
169
|
+
// authoritative >=1M window (Claude Code then accounts 1M, compaction preserved).
|
|
170
|
+
for (const [name, value] of Object.entries(effectiveModelEnv(config.claudeCode, contextWindows, auto))) {
|
|
171
|
+
setDefault(name, value);
|
|
172
|
+
}
|
|
173
|
+
return env;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Context-window map from the RUNNING proxy's management API (warm TTL cache; the
|
|
178
|
+
* daemon registers every selector form — audit R3#1). 3s bound + management auth header.
|
|
179
|
+
* (no [1m] marking, conservative).
|
|
180
|
+
*/
|
|
181
|
+
export async function fetchClaudeContextWindows(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<Record<string, number>> {
|
|
182
|
+
try {
|
|
183
|
+
const headers = new Headers();
|
|
184
|
+
const token = configuredAdminToken();
|
|
185
|
+
if (token) headers.set("x-opencodex-api-key", token);
|
|
186
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/claude-code`, {
|
|
187
|
+
headers,
|
|
188
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) return {};
|
|
191
|
+
const body = await res.json() as { contextWindows?: Record<string, number> };
|
|
192
|
+
return body.contextWindows && typeof body.contextWindows === "object" ? body.contextWindows : {};
|
|
193
|
+
} catch {
|
|
194
|
+
console.error("⚠ 모델 컨텍스트 정보를 불러오지 못했습니다 — 1M 자동 표시는 이번 실행에서 생략됩니다.");
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function ensureProxyForClaude(): Promise<number | null> {
|
|
200
|
+
const live = await findLiveProxy();
|
|
201
|
+
if (live) return live.port;
|
|
202
|
+
const cfgPort = loadConfig().port;
|
|
203
|
+
const pinPort = typeof cfgPort === "number" && cfgPort > 0 ? cfgPort : 10100;
|
|
204
|
+
const child = spawn(process.execPath, [process.argv[1], "start", "--port", String(pinPort)], {
|
|
205
|
+
detached: true,
|
|
206
|
+
stdio: "ignore",
|
|
207
|
+
windowsHide: true,
|
|
208
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
209
|
+
});
|
|
210
|
+
child.unref();
|
|
211
|
+
const deadline = Date.now() + 8_000;
|
|
212
|
+
while (Date.now() < deadline) {
|
|
213
|
+
const started = await findLiveProxy();
|
|
214
|
+
if (started) return started.port;
|
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const CLAUDE_INSTALL_HINT = "❌ `claude` CLI not found. Install it first: npm install -g @anthropic-ai/claude-code";
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* cmd.exe reports command-not-found as exit 9009 (the win32 launcher routes `.cmd`
|
|
224
|
+
* shims through cmd.exe, so ENOENT never fires there). Signal exits are not hints.
|
|
225
|
+
* Devlog 260715_cross_platform_audit/020.
|
|
226
|
+
*/
|
|
227
|
+
export function claudeNotFoundHint(
|
|
228
|
+
code: number | null,
|
|
229
|
+
signal: NodeJS.Signals | null,
|
|
230
|
+
platform: NodeJS.Platform = process.platform,
|
|
231
|
+
): string | null {
|
|
232
|
+
return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function cmdClaude(args: string[]): Promise<number> {
|
|
236
|
+
const config = loadConfig();
|
|
237
|
+
if (config.claudeCode?.enabled === false) {
|
|
238
|
+
console.error("Claude inbound is disabled (config.claudeCode.enabled=false — flip the Claude ON toggle in the GUI or edit config).");
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const port = await ensureProxyForClaude();
|
|
242
|
+
if (!port) {
|
|
243
|
+
console.error("❌ Proxy did not become healthy after starting.");
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
const contextWindows = await fetchClaudeContextWindows(config, port);
|
|
247
|
+
const env = buildClaudeEnv(config, port, process.env, contextWindows);
|
|
248
|
+
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
|
|
249
|
+
// never refreshes it, so the picker would keep showing yesterday's aliases.
|
|
250
|
+
try {
|
|
251
|
+
const cachePath = await refreshGatewayModelCacheFromProxy(port);
|
|
252
|
+
if (cachePath === null) {
|
|
253
|
+
console.error("⚠ Gateway model cache could not be refreshed; the model picker may be stale.");
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
console.error(`⚠ Gateway model cache could not be refreshed: ${message}`);
|
|
258
|
+
}
|
|
259
|
+
// Sync roster agents (devlog 070): subagentModels + self -> ~/.claude/agents/ocx-*.md.
|
|
260
|
+
try {
|
|
261
|
+
const written = injectClaudeAgentDefs(config, contextWindows);
|
|
262
|
+
if (written === null) {
|
|
263
|
+
console.error("⚠ Claude agent definitions could not be synced; check ~/.claude/agents permissions.");
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
267
|
+
console.error(`⚠ Claude agent definitions could not be synced: ${message}`);
|
|
268
|
+
}
|
|
269
|
+
return await new Promise<number>(resolve => {
|
|
270
|
+
const inv = commandInvocation("claude", args);
|
|
271
|
+
const child = spawn(inv.file, inv.args, { stdio: "inherit", env: env as NodeJS.ProcessEnv, ...inv.options });
|
|
272
|
+
child.on("error", (err: NodeJS.ErrnoException) => {
|
|
273
|
+
if (err.code === "ENOENT") {
|
|
274
|
+
console.error(CLAUDE_INSTALL_HINT);
|
|
275
|
+
} else {
|
|
276
|
+
console.error(`❌ Failed to launch claude: ${err.message}`);
|
|
277
|
+
}
|
|
278
|
+
resolve(1);
|
|
279
|
+
});
|
|
280
|
+
child.on("exit", (code, signal) => {
|
|
281
|
+
const hint = claudeNotFoundHint(code, signal);
|
|
282
|
+
if (hint) console.error(hint);
|
|
283
|
+
resolve(signal ? 1 : code ?? 0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { autoRestoreCodexShim } from "../codex/shim";
|
|
2
|
+
import { codexShimAutoRestoreEnabled, readConfigDiagnostics } from "../config";
|
|
3
|
+
|
|
4
|
+
export interface CodexShimAutoRestoreCliDeps {
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
warn: (message: string) => void;
|
|
7
|
+
restore: typeof autoRestoreCodexShim;
|
|
8
|
+
readConfig: typeof readConfigDiagnostics;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
|
|
12
|
+
env: process.env,
|
|
13
|
+
warn: message => console.warn(message),
|
|
14
|
+
restore: autoRestoreCodexShim,
|
|
15
|
+
readConfig: readConfigDiagnostics,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
|
|
19
|
+
if (command === "uninstall" || command === "remove") return true;
|
|
20
|
+
return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function maybeAutoRestoreCodexShim(
|
|
24
|
+
command: string | undefined,
|
|
25
|
+
args: string[],
|
|
26
|
+
deps: CodexShimAutoRestoreCliDeps = DEFAULT_DEPS,
|
|
27
|
+
): void {
|
|
28
|
+
if (skipsCodexShimAutoRestore(command, args)) return;
|
|
29
|
+
try {
|
|
30
|
+
const result = deps.restore({
|
|
31
|
+
enabled: () => codexShimAutoRestoreEnabled(deps.readConfig().config, deps.env),
|
|
32
|
+
});
|
|
33
|
+
if (result.status === "restored") {
|
|
34
|
+
deps.warn(`⚠️ ${result.message} (automatic repair after Codex update)`);
|
|
35
|
+
} else if ((result.status === "deferred" || result.status === "ineligible") && result.message) {
|
|
36
|
+
deps.warn(`⚠️ ${result.message}`);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
deps.warn(
|
|
40
|
+
`⚠️ Codex shim auto-restore failed; continuing without it: ${
|
|
41
|
+
error instanceof Error ? error.message : String(error)
|
|
42
|
+
}. Run 'ocx codex-shim install' after the Codex update finishes.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/cli/combo.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CliUsageError,
|
|
3
|
+
printData,
|
|
4
|
+
rejectArgs,
|
|
5
|
+
runCliAction,
|
|
6
|
+
runtimeRequest,
|
|
7
|
+
takeFlag,
|
|
8
|
+
takeIntegerOption,
|
|
9
|
+
takeOption,
|
|
10
|
+
type RuntimeApiDeps,
|
|
11
|
+
} from "./runtime-api";
|
|
12
|
+
|
|
13
|
+
const USAGE = `Usage:
|
|
14
|
+
ocx combo [list] [--json]
|
|
15
|
+
ocx combo show <id> [--json]
|
|
16
|
+
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
|
+
[--strategy <failover|round-robin>] [--sticky <1-100>]
|
|
18
|
+
[--effort <low|medium|high|xhigh|max|ultra|->] [--alias <name|->]
|
|
19
|
+
[--rename-from <id>] [--json]
|
|
20
|
+
ocx combo remove <id> --yes [--json]`;
|
|
21
|
+
|
|
22
|
+
type ComboRow = Record<string, unknown> & { id?: string; model?: string };
|
|
23
|
+
|
|
24
|
+
function parseTargets(value: string): Array<{ provider: string; model: string; weight?: number }> {
|
|
25
|
+
const targets = value.split(",").map(part => part.trim()).filter(Boolean).map(part => {
|
|
26
|
+
const colon = part.lastIndexOf(":");
|
|
27
|
+
let selector = part;
|
|
28
|
+
let weight: number | undefined;
|
|
29
|
+
if (colon > part.indexOf("/")) {
|
|
30
|
+
const maybeWeight = Number(part.slice(colon + 1));
|
|
31
|
+
if (Number.isInteger(maybeWeight)) {
|
|
32
|
+
selector = part.slice(0, colon);
|
|
33
|
+
weight = maybeWeight;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const slash = selector.indexOf("/");
|
|
37
|
+
if (slash <= 0 || slash === selector.length - 1) throw new CliUsageError(`invalid target "${part}"; use provider/model[:weight]`, USAGE);
|
|
38
|
+
const target = { provider: selector.slice(0, slash), model: selector.slice(slash + 1), ...(weight !== undefined ? { weight } : {}) };
|
|
39
|
+
if (weight !== undefined && (weight < 1 || weight > 10_000)) throw new CliUsageError(`target weight must be 1-10000: ${part}`, USAGE);
|
|
40
|
+
return target;
|
|
41
|
+
});
|
|
42
|
+
if (targets.length === 0) throw new CliUsageError("--targets requires at least one provider/model", USAGE);
|
|
43
|
+
return targets;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function list(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
47
|
+
const args = [...argv];
|
|
48
|
+
const wantsJson = takeFlag(args, "--json");
|
|
49
|
+
rejectArgs(args, USAGE);
|
|
50
|
+
const result = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
51
|
+
const rows = result.combos ?? [];
|
|
52
|
+
printData(result, wantsJson, rows.length ? rows.map(row => `${String(row.id)} ${String(row.model ?? `combo/${row.id}`)}`) : ["No combos configured."]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
56
|
+
const args = [...argv];
|
|
57
|
+
const id = args.shift();
|
|
58
|
+
const wantsJson = takeFlag(args, "--json");
|
|
59
|
+
if (!id) throw new CliUsageError("combo id is required", USAGE);
|
|
60
|
+
rejectArgs(args, USAGE);
|
|
61
|
+
const result = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
62
|
+
const combo = (result.combos ?? []).find(row => row.id === id);
|
|
63
|
+
if (!combo) throw new CliUsageError(`unknown combo ${id}`);
|
|
64
|
+
printData(combo, wantsJson);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
68
|
+
const args = [...argv];
|
|
69
|
+
const id = args.shift()?.trim();
|
|
70
|
+
const wantsJson = takeFlag(args, "--json");
|
|
71
|
+
if (!id) throw new CliUsageError("combo id is required", USAGE);
|
|
72
|
+
const targetsRaw = takeOption(args, "--targets");
|
|
73
|
+
if (!targetsRaw) throw new CliUsageError("--targets is required", USAGE);
|
|
74
|
+
const strategy = takeOption(args, "--strategy") ?? "failover";
|
|
75
|
+
if (strategy !== "failover" && strategy !== "round-robin") throw new CliUsageError("--strategy must be failover or round-robin", USAGE);
|
|
76
|
+
const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 }) ?? 1;
|
|
77
|
+
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
78
|
+
const effort = takeOption(args, "--effort");
|
|
79
|
+
const alias = takeOption(args, "--alias");
|
|
80
|
+
const renameFrom = takeOption(args, "--rename-from");
|
|
81
|
+
rejectArgs(args, USAGE);
|
|
82
|
+
const combo: Record<string, unknown> = {
|
|
83
|
+
strategy,
|
|
84
|
+
stickyLimit,
|
|
85
|
+
targets: parseTargets(targetsRaw),
|
|
86
|
+
};
|
|
87
|
+
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|
|
88
|
+
if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
|
|
89
|
+
const result = await runtimeRequest("/api/combos", {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
|
|
92
|
+
}, deps);
|
|
93
|
+
printData(result, wantsJson, [`Saved combo ${id}.`]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function remove(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
97
|
+
const args = [...argv];
|
|
98
|
+
const id = args.shift()?.trim();
|
|
99
|
+
const wantsJson = takeFlag(args, "--json");
|
|
100
|
+
const yes = takeFlag(args, "--yes");
|
|
101
|
+
if (!id) throw new CliUsageError("combo id is required", USAGE);
|
|
102
|
+
if (!yes) throw new CliUsageError("remove requires --yes", USAGE);
|
|
103
|
+
rejectArgs(args, USAGE);
|
|
104
|
+
const result = await runtimeRequest(`/api/combos?id=${encodeURIComponent(id)}`, { method: "DELETE" }, deps);
|
|
105
|
+
printData(result, wantsJson, [`Removed combo ${id}.`]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
109
|
+
return runCliAction(async () => {
|
|
110
|
+
const [sub = "list", ...rest] = argv;
|
|
111
|
+
if (sub === "list") await list(rest, deps);
|
|
112
|
+
else if (sub === "show") await show(rest, deps);
|
|
113
|
+
else if (sub === "set" || sub === "create" || sub === "update") await set(rest, deps);
|
|
114
|
+
else if (sub === "remove" || sub === "delete") await remove(rest, deps);
|
|
115
|
+
else throw new CliUsageError(`unknown combo command ${sub}`, USAGE);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const COMBO_USAGE = USAGE;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { getConfigPath, readConfigDiagnostics, saveConfig, validateConfigCandidate } from "../config";
|
|
3
|
+
import type { OcxConfig } from "../types";
|
|
4
|
+
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage:
|
|
7
|
+
ocx config [show] [--json] [--source]
|
|
8
|
+
ocx config get <dot.path> [--json]
|
|
9
|
+
ocx config set <dot.path> <json-or-string> [--json]
|
|
10
|
+
ocx config unset <dot.path> [--json]
|
|
11
|
+
ocx config validate [path|-] [--json]
|
|
12
|
+
ocx config export <path|->
|
|
13
|
+
ocx config import <path|-> --yes [--json]`;
|
|
14
|
+
|
|
15
|
+
const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret)$/i;
|
|
16
|
+
const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
17
|
+
|
|
18
|
+
function redact(value: unknown, key = ""): unknown {
|
|
19
|
+
if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
|
|
20
|
+
if (Array.isArray(value)) return value.map(item => redact(item));
|
|
21
|
+
if (value && typeof value === "object") {
|
|
22
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([childKey, child]) => [childKey, redact(child, childKey)]));
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pathSegments(path: string): string[] {
|
|
28
|
+
const segments = path.split(".").map(part => part.trim()).filter(Boolean);
|
|
29
|
+
if (segments.length === 0 || segments.some(part => BLOCKED_SEGMENTS.has(part))) throw new CliUsageError("invalid config path", USAGE);
|
|
30
|
+
return segments;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPath(root: unknown, path: string): unknown {
|
|
34
|
+
let current = root;
|
|
35
|
+
for (const segment of pathSegments(path)) {
|
|
36
|
+
if (!current || typeof current !== "object" || !Object.hasOwn(current, segment)) throw new CliUsageError(`config path not found: ${path}`);
|
|
37
|
+
current = (current as Record<string, unknown>)[segment];
|
|
38
|
+
}
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setPath(root: Record<string, unknown>, path: string, value: unknown, remove = false): void {
|
|
43
|
+
const segments = pathSegments(path);
|
|
44
|
+
let current = root;
|
|
45
|
+
for (const segment of segments.slice(0, -1)) {
|
|
46
|
+
const next = current[segment];
|
|
47
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) throw new CliUsageError(`config parent path not found: ${segment}`, USAGE);
|
|
48
|
+
current = next as Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
const leaf = segments.at(-1)!;
|
|
51
|
+
if (remove && !Object.hasOwn(current, leaf)) throw new CliUsageError(`config path not found: ${path}`);
|
|
52
|
+
if (remove) delete current[leaf];
|
|
53
|
+
else current[leaf] = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseValue(raw: string): unknown {
|
|
57
|
+
try { return JSON.parse(raw); }
|
|
58
|
+
catch { return raw; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadInput(path: string): unknown {
|
|
62
|
+
const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
|
|
63
|
+
try { return JSON.parse(raw); }
|
|
64
|
+
catch { throw new CliUsageError(`invalid JSON in ${path}`); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validate(value: unknown): OcxConfig {
|
|
68
|
+
const result = validateConfigCandidate(value);
|
|
69
|
+
if (!result.ok) throw new CliUsageError(result.error);
|
|
70
|
+
return result.config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
74
|
+
return runCliAction(async () => {
|
|
75
|
+
const args = [...argv];
|
|
76
|
+
const action = (args.shift() ?? "show").toLowerCase();
|
|
77
|
+
const wantsJson = takeFlag(args, "--json");
|
|
78
|
+
if (action === "show") {
|
|
79
|
+
const source = takeFlag(args, "--source");
|
|
80
|
+
rejectArgs(args, USAGE);
|
|
81
|
+
const diagnostics = readConfigDiagnostics();
|
|
82
|
+
const config = redact(diagnostics.config);
|
|
83
|
+
const result = source ? { config, source: diagnostics.source, error: diagnostics.error, warnings: diagnostics.warnings ?? [] } : config;
|
|
84
|
+
printData(result, true);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (action === "get") {
|
|
88
|
+
const path = args.shift();
|
|
89
|
+
if (!path) throw new CliUsageError("config path is required", USAGE);
|
|
90
|
+
rejectArgs(args, USAGE);
|
|
91
|
+
const value = redact(getPath(readConfigDiagnostics().config, path), path.split(".").at(-1));
|
|
92
|
+
if (wantsJson || typeof value === "object") console.log(JSON.stringify(value, null, 2));
|
|
93
|
+
else console.log(String(value));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (action === "set" || action === "unset") {
|
|
97
|
+
const path = args.shift();
|
|
98
|
+
const raw = action === "set" ? args.shift() : undefined;
|
|
99
|
+
if (!path || (action === "set" && raw === undefined)) throw new CliUsageError("config path and value are required", USAGE);
|
|
100
|
+
rejectArgs(args, USAGE);
|
|
101
|
+
const candidate = structuredClone(readConfigDiagnostics().config) as unknown as Record<string, unknown>;
|
|
102
|
+
setPath(candidate, path, raw === undefined ? undefined : parseValue(raw), action === "unset");
|
|
103
|
+
const config = validate(candidate);
|
|
104
|
+
const savedValue = action === "unset" ? null : getPath(config, path);
|
|
105
|
+
saveConfig(config);
|
|
106
|
+
printData({ ok: true, path, value: redact(savedValue, path.split(".").at(-1)) }, wantsJson,
|
|
107
|
+
[`${action === "unset" ? "Unset" : "Set"} ${path}.`]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (action === "validate") {
|
|
111
|
+
const path = args.shift();
|
|
112
|
+
rejectArgs(args, USAGE);
|
|
113
|
+
const result = path ? validateConfigCandidate(loadInput(path)) : (() => {
|
|
114
|
+
const diagnostics = readConfigDiagnostics();
|
|
115
|
+
return diagnostics.error ? { ok: false as const, error: diagnostics.error } : { ok: true as const, config: diagnostics.config };
|
|
116
|
+
})();
|
|
117
|
+
printData(result.ok ? { ok: true, source: path ?? getConfigPath() } : result, wantsJson,
|
|
118
|
+
[result.ok ? "Config is valid." : `Config is invalid: ${result.error}`]);
|
|
119
|
+
if (!result.ok) process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (action === "export") {
|
|
123
|
+
const path = args.shift();
|
|
124
|
+
if (!path) throw new CliUsageError("export path is required", USAGE);
|
|
125
|
+
rejectArgs(args, USAGE);
|
|
126
|
+
const content = `${JSON.stringify(readConfigDiagnostics().config, null, 2)}\n`;
|
|
127
|
+
if (path === "-") process.stdout.write(content);
|
|
128
|
+
else { writeFileSync(path, content, { encoding: "utf8", mode: 0o600 }); console.log(`Exported config to ${path}.`); }
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (action === "import") {
|
|
132
|
+
const path = args.shift();
|
|
133
|
+
const yes = takeFlag(args, "--yes");
|
|
134
|
+
if (!path) throw new CliUsageError("import path is required", USAGE);
|
|
135
|
+
if (!yes) throw new CliUsageError("import requires --yes", USAGE);
|
|
136
|
+
rejectArgs(args, USAGE);
|
|
137
|
+
saveConfig(validate(loadInput(path)));
|
|
138
|
+
printData({ ok: true, source: path }, wantsJson, [`Imported config from ${path}. Restart or run ocx sync if needed.`]);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
throw new CliUsageError(`unknown config command ${action}`, USAGE);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const CONFIG_USAGE = USAGE;
|