@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,381 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { atomicWriteFile } from "../config";
|
|
6
|
+
import type { OcxClaudeDesktopProfile } from "../types";
|
|
7
|
+
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
8
|
+
import {
|
|
9
|
+
reconcileDesktopProfile,
|
|
10
|
+
renderDesktopProfile,
|
|
11
|
+
type DesktopProfileModel,
|
|
12
|
+
} from "./desktop-profile";
|
|
13
|
+
import { nativeOpenAiContextWindow } from "../codex/catalog";
|
|
14
|
+
import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
|
|
15
|
+
|
|
16
|
+
export interface Desktop3pModelEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
labelOverride: string;
|
|
19
|
+
anthropicFamilyTier: "opus" | "fable" | "sonnet" | "haiku";
|
|
20
|
+
isFamilyDefault?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Desktop's documented 1M-context capability assertion. Set ONLY from an
|
|
23
|
+
* authoritative routed contextWindow >= 1M — never guessed (devlog 136 B5).
|
|
24
|
+
*/
|
|
25
|
+
supports1m?: true;
|
|
26
|
+
/** When true, Desktop selects the 1M variant by default (official schema, Luna research 260722). */
|
|
27
|
+
prefer1m?: true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* static (default, Pro-verified devlog 138): pinned inferenceModels with
|
|
32
|
+
* modelDiscoveryEnabled:false — a static list OVERRIDES discovery (no merge), so
|
|
33
|
+
* this is the deterministic shape. hybrid keeps discovery:true alongside the list
|
|
34
|
+
* (claude-code-router's version-defensive pattern). discovery: /v1/models only.
|
|
35
|
+
*/
|
|
36
|
+
export type Desktop3pConfigMode = "hybrid" | "discovery" | "static";
|
|
37
|
+
|
|
38
|
+
export interface Desktop3pRoutedModel {
|
|
39
|
+
provider: string;
|
|
40
|
+
id: string;
|
|
41
|
+
/** Authoritative context window (CatalogModel.contextWindow); optional. */
|
|
42
|
+
contextWindow?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 1M-context eligibility, shared with the Desktop DTO so the dashboard's 1M chip can
|
|
47
|
+
* never disagree with what the writer emits. The DTO imports this from here — keeping
|
|
48
|
+
* the constant in this module avoids a cycle, since shared.ts already reads
|
|
49
|
+
* claude/desktop-profile.
|
|
50
|
+
*/
|
|
51
|
+
export const DESKTOP_SUPPORTS_1M_THRESHOLD = 1_000_000;
|
|
52
|
+
|
|
53
|
+
export interface Desktop3pConfigLibraryOptions {
|
|
54
|
+
env?: NodeJS.ProcessEnv;
|
|
55
|
+
platform?: NodeJS.Platform;
|
|
56
|
+
homeDir?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the config library from the same user-data root Claude Desktop uses. Keeping this in one
|
|
61
|
+
* helper prevents the writer and dashboard status probe from agreeing on a path Desktop never reads.
|
|
62
|
+
*
|
|
63
|
+
* The resolution itself lives in `./desktop-3p-paths`, which ports Claude Desktop's own `GE()`
|
|
64
|
+
* branch for branch — including the `-3p` suffix the app appends to its userData root. Dropping
|
|
65
|
+
* that suffix points us at a directory Desktop never reads (GitHub #539).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDesktop3pConfigLibraryPath(
|
|
68
|
+
options: Desktop3pConfigLibraryOptions = {},
|
|
69
|
+
): string {
|
|
70
|
+
if (options.env === undefined && options.platform === undefined && options.homeDir === undefined) {
|
|
71
|
+
return claudeDesktopConfigLibraryDir();
|
|
72
|
+
}
|
|
73
|
+
return resolveConfigLibraryDir({
|
|
74
|
+
env: options.env ?? process.env,
|
|
75
|
+
platform: options.platform ?? process.platform,
|
|
76
|
+
home: options.homeDir ?? homedir(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
|
|
81
|
+
export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
|
|
82
|
+
const known = new Map<string, Desktop3pConfigMode>([
|
|
83
|
+
["--static", "static"],
|
|
84
|
+
["--hybrid", "hybrid"],
|
|
85
|
+
["--discovery-only", "discovery"],
|
|
86
|
+
]);
|
|
87
|
+
const unknown = flags.filter(a => !known.has(a));
|
|
88
|
+
if (unknown.length > 0) return { error: `알 수 없는 옵션: ${unknown.join(" ")} (지원: --static, --hybrid, --discovery-only)` };
|
|
89
|
+
const picked = [...new Set(flags.map(a => known.get(a)!))];
|
|
90
|
+
if (picked.length > 1) return { error: "모드 옵션은 하나만 쓸 수 있습니다 (--static | --hybrid | --discovery-only)." };
|
|
91
|
+
return { mode: picked[0] ?? "static" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Desktop3pMetadataEntry {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface Desktop3pMetadata {
|
|
101
|
+
appliedId?: string;
|
|
102
|
+
entries: Desktop3pMetadataEntry[];
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let desktop3pRegistry = new Map<string, string>();
|
|
107
|
+
let desktop3pAliasesByRoute = new Map<string, string>();
|
|
108
|
+
|
|
109
|
+
/** Derive a stable letter-first, three-character base36 code from a route key. */
|
|
110
|
+
export function deriveDesktop3pCode(route: string): string {
|
|
111
|
+
const hash = createHash("sha256").update(route).digest();
|
|
112
|
+
const n = hash.readUInt32BE(0) % 33696;
|
|
113
|
+
const first = String.fromCharCode(97 + Math.floor(n / 1296));
|
|
114
|
+
const rest = (n % 1296).toString(36).padStart(2, "0");
|
|
115
|
+
return first + rest;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Alias for one proxy model. Real Anthropic models pass through unchanged (they must
|
|
120
|
+
* keep hitting the sk-ant native passthrough); everything else gets a Claude-shaped
|
|
121
|
+
* `claude-opus-4-8-{code}` id. Opus 4.8 is chosen deliberately: Desktop's effort
|
|
122
|
+
* selector is an allowlist keyed on exact supported model ids (Opus 4.8/4.7/4.6,
|
|
123
|
+
* Sonnet 4.6 — devlog 131), and 4.6+ canonical ids are dateless, so the letter-first
|
|
124
|
+
* 3-char suffix can never collide with a real id or a legacy date suffix.
|
|
125
|
+
*/
|
|
126
|
+
export function desktop3pAlias(provider: string, modelId: string): string {
|
|
127
|
+
if (provider === "anthropic" && modelId.startsWith("claude-")) return modelId;
|
|
128
|
+
return `claude-opus-4-8-${deriveDesktop3pCode(`${provider}/${modelId}`)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Pre-rename alias shape (claude-opus-4-{code}) — still decoded for stale Desktop configs. */
|
|
132
|
+
export function legacyDesktop3pAlias(provider: string, modelId: string): string {
|
|
133
|
+
return `claude-opus-4-${deriveDesktop3pCode(`${provider}/${modelId}`)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function displayModelId(modelId: string): string {
|
|
137
|
+
return modelId
|
|
138
|
+
// Capability markers like [1m] are not name text: strip the brackets so the label
|
|
139
|
+
// reads "K3 1M", never "K3[1m]".
|
|
140
|
+
.replace(/\[([^\]]+)\]/g, "-$1")
|
|
141
|
+
.split(/[-_]+/)
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.map(part => {
|
|
144
|
+
const lower = part.toLowerCase();
|
|
145
|
+
if (lower === "gpt" || lower === "glm" || lower === "ai" || lower === "1m") return lower.toUpperCase();
|
|
146
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
147
|
+
})
|
|
148
|
+
.join(" ");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function collectDesktop3pModels(
|
|
152
|
+
nativeSlugs: string[],
|
|
153
|
+
routedModels: Array<Desktop3pRoutedModel>,
|
|
154
|
+
profile?: OcxClaudeDesktopProfile,
|
|
155
|
+
): { models: Desktop3pModelEntry[]; registry: Map<string, string> } {
|
|
156
|
+
const registry = new Map<string, string>();
|
|
157
|
+
const models: Desktop3pModelEntry[] = [];
|
|
158
|
+
const candidates: Desktop3pRoutedModel[] = [
|
|
159
|
+
// Native candidates carry their real context window from the same accessor the
|
|
160
|
+
// Desktop DTO uses, so a native 1M/372k model resolves identically in the written
|
|
161
|
+
// config and on the dashboard.
|
|
162
|
+
...nativeSlugs.map(id => {
|
|
163
|
+
const contextWindow = nativeOpenAiContextWindow(id);
|
|
164
|
+
return { provider: "native", id, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
165
|
+
}),
|
|
166
|
+
...routedModels,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
if (profile) {
|
|
170
|
+
const profileModels = candidates.map(({ provider, id, contextWindow }) => ({
|
|
171
|
+
route: `${provider}/${id}`,
|
|
172
|
+
label: `${displayModelId(id)} (${provider})`,
|
|
173
|
+
...(typeof contextWindow === "number" ? { contextWindow } : {}),
|
|
174
|
+
} satisfies DesktopProfileModel));
|
|
175
|
+
const reconciled = reconcileDesktopProfile(profile, profileModels);
|
|
176
|
+
const rendered = renderDesktopProfile(reconciled, profileModels);
|
|
177
|
+
const aliasesByRoute = new Map<string, string>();
|
|
178
|
+
for (const model of rendered) {
|
|
179
|
+
aliasesByRoute.set(model.route, model.name);
|
|
180
|
+
if (!model.route.startsWith("anthropic/claude-")) registry.set(model.name, model.route);
|
|
181
|
+
models.push({
|
|
182
|
+
name: model.name,
|
|
183
|
+
labelOverride: model.label,
|
|
184
|
+
anthropicFamilyTier: model.family,
|
|
185
|
+
...(model.isFamilyDefault ? { isFamilyDefault: true } : {}),
|
|
186
|
+
...(model.supports1m ? { supports1m: true, prefer1m: true } : {}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Legacy hashes are compatibility-only and can collide. Bind them in stable route order so
|
|
190
|
+
// changing a family default or rendered ordering can never silently rebind an old Desktop id.
|
|
191
|
+
for (const model of [...rendered].sort((a, b) => a.route.localeCompare(b.route))) {
|
|
192
|
+
if (model.route.startsWith("anthropic/claude-")) continue;
|
|
193
|
+
const providerEnd = model.route.indexOf("/");
|
|
194
|
+
const provider = model.route.slice(0, providerEnd);
|
|
195
|
+
const id = model.route.slice(providerEnd + 1);
|
|
196
|
+
const legacy = legacyDesktop3pAlias(provider, id);
|
|
197
|
+
const existing = registry.get(legacy);
|
|
198
|
+
if (existing && existing !== model.route) {
|
|
199
|
+
console.warn(`[opencodex] Claude Desktop legacy alias collision: ${legacy} stays bound to ${existing}; ignoring ${model.route}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
registry.set(legacy, model.route);
|
|
203
|
+
}
|
|
204
|
+
desktop3pAliasesByRoute = aliasesByRoute;
|
|
205
|
+
return { models, registry };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const { provider, id, contextWindow } of candidates) {
|
|
209
|
+
const route = `${provider}/${id}`;
|
|
210
|
+
const alias = desktop3pAlias(provider, id);
|
|
211
|
+
const supports1m = typeof contextWindow === "number" && contextWindow >= DESKTOP_SUPPORTS_1M_THRESHOLD
|
|
212
|
+
? { supports1m: true as const }
|
|
213
|
+
: {};
|
|
214
|
+
if (alias === id) {
|
|
215
|
+
// Real Anthropic model: keep it OUT of the decode registry — registering it would
|
|
216
|
+
// make resolveInboundModel() non-identity and kill the sk-ant native passthrough
|
|
217
|
+
// (audit 133 #1). It still appears in the static Desktop model list below.
|
|
218
|
+
models.push({
|
|
219
|
+
name: alias,
|
|
220
|
+
labelOverride: `${displayModelId(id)} (${provider})`,
|
|
221
|
+
anthropicFamilyTier: "opus",
|
|
222
|
+
...supports1m,
|
|
223
|
+
...(supports1m.supports1m ? { prefer1m: true as const } : {}),
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const existingRoute = registry.get(alias);
|
|
228
|
+
if (existingRoute !== undefined) {
|
|
229
|
+
console.warn(`[opencodex] Claude Desktop 3P alias collision: ${alias} maps to both ${existingRoute} and ${route}; skipping ${route}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
registry.set(alias, route);
|
|
234
|
+
// Back-compat decode for Desktop configs written before the opus-4-8 rename.
|
|
235
|
+
const legacy = legacyDesktop3pAlias(provider, id);
|
|
236
|
+
if (!registry.has(legacy)) registry.set(legacy, route);
|
|
237
|
+
models.push({
|
|
238
|
+
name: alias,
|
|
239
|
+
labelOverride: `${displayModelId(id)} (${provider})`,
|
|
240
|
+
anthropicFamilyTier: "opus",
|
|
241
|
+
...supports1m,
|
|
242
|
+
...(supports1m.supports1m ? { prefer1m: true as const } : {}),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (models[0]) models[0].isFamilyDefault = true;
|
|
247
|
+
desktop3pAliasesByRoute = new Map(candidates.map(({ provider, id }) => [`${provider}/${id}`, desktop3pAlias(provider, id)]));
|
|
248
|
+
return { models, registry };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Build and install the registry used to decode Desktop aliases. */
|
|
252
|
+
export function buildDesktop3pRegistry(
|
|
253
|
+
nativeSlugs: string[],
|
|
254
|
+
routedModels: Array<Desktop3pRoutedModel>,
|
|
255
|
+
profile?: OcxClaudeDesktopProfile,
|
|
256
|
+
): Map<string, string> {
|
|
257
|
+
const { registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
258
|
+
desktop3pRegistry = registry;
|
|
259
|
+
return registry;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Generate Claude Desktop 3P model entries from the proxy's available models. */
|
|
263
|
+
export function generateDesktop3pModels(
|
|
264
|
+
nativeSlugs: string[],
|
|
265
|
+
routedModels: Array<Desktop3pRoutedModel>,
|
|
266
|
+
profile?: OcxClaudeDesktopProfile,
|
|
267
|
+
): Desktop3pModelEntry[] {
|
|
268
|
+
const { models, registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
269
|
+
desktop3pRegistry = registry;
|
|
270
|
+
return models;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Resolve an alias using the most recently generated Desktop model registry. */
|
|
274
|
+
export function resolveDesktop3pAlias(alias: string): string | null {
|
|
275
|
+
return desktop3pRegistry.get(alias) ?? null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Alias selected by the installed profile registry, falling back to the legacy hash shape. */
|
|
279
|
+
export function activeDesktop3pAlias(provider: string, modelId: string): string {
|
|
280
|
+
return desktop3pAliasesByRoute.get(`${provider}/${modelId}`) ?? desktop3pAlias(provider, modelId);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate the complete Claude Desktop 3P gateway config.
|
|
285
|
+
*
|
|
286
|
+
* Default mode is "static" (Pro-verified, devlog 138): the static list is the ONLY
|
|
287
|
+
* channel for supports1m/tier pins and it overrides discovery anyway (no merge), so
|
|
288
|
+
* discovery stays off for determinism. supports1m makes Desktop offer a separate 1M
|
|
289
|
+
* row; selecting it sends the bare id + `anthropic-beta: context-1m-2025-08-07`.
|
|
290
|
+
*/
|
|
291
|
+
export function generateDesktop3pConfig(
|
|
292
|
+
port: number,
|
|
293
|
+
nativeSlugs: string[],
|
|
294
|
+
routedModels: Array<Desktop3pRoutedModel>,
|
|
295
|
+
apiKey = "ocx",
|
|
296
|
+
mode: Desktop3pConfigMode = "static",
|
|
297
|
+
profile?: OcxClaudeDesktopProfile,
|
|
298
|
+
): object {
|
|
299
|
+
const base = {
|
|
300
|
+
inferenceProvider: "gateway",
|
|
301
|
+
inferenceCredentialKind: "static",
|
|
302
|
+
inferenceGatewayBaseUrl: `http://127.0.0.1:${port}`,
|
|
303
|
+
inferenceGatewayApiKey: apiKey,
|
|
304
|
+
};
|
|
305
|
+
if (mode === "discovery") {
|
|
306
|
+
// Build/refresh the decode registry even though no static list is emitted.
|
|
307
|
+
buildDesktop3pRegistry(nativeSlugs, routedModels, profile);
|
|
308
|
+
return { ...base, modelDiscoveryEnabled: true };
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
...base,
|
|
312
|
+
modelDiscoveryEnabled: mode === "hybrid",
|
|
313
|
+
inferenceModels: (() => {
|
|
314
|
+
const models = generateDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
315
|
+
// Fail loud at the write boundary rather than ship a config Desktop rejects:
|
|
316
|
+
// the output counterpart of the request-path guards.
|
|
317
|
+
assertDesktop3pModelsValid(models);
|
|
318
|
+
return models;
|
|
319
|
+
})(),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseMetadata(path: string): Desktop3pMetadata {
|
|
324
|
+
if (!existsSync(path)) return { entries: [] };
|
|
325
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Desktop3pMetadata>;
|
|
326
|
+
if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
|
|
327
|
+
return { ...parsed, entries: parsed.entries };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Write and apply the opencodex config in Claude Desktop 3P's config library. */
|
|
331
|
+
export function writeDesktop3pConfig(
|
|
332
|
+
port: number,
|
|
333
|
+
nativeSlugs: string[],
|
|
334
|
+
routedModels: Array<Desktop3pRoutedModel>,
|
|
335
|
+
apiKey?: string,
|
|
336
|
+
mode: Desktop3pConfigMode = "static",
|
|
337
|
+
profile?: OcxClaudeDesktopProfile,
|
|
338
|
+
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
|
|
339
|
+
const libraryPath = resolveDesktop3pConfigLibraryPath();
|
|
340
|
+
const metadataPath = join(libraryPath, "_meta.json");
|
|
341
|
+
let configPath = libraryPath;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
|
|
345
|
+
const metadata = parseMetadata(metadataPath);
|
|
346
|
+
const existing = metadata.entries.find(entry => entry?.name === "opencodex" && typeof entry.id === "string");
|
|
347
|
+
const id = existing?.id ?? randomUUID();
|
|
348
|
+
configPath = join(libraryPath, `${id}.json`);
|
|
349
|
+
const entry: Desktop3pMetadataEntry = existing ? { ...existing, id, name: "opencodex" } : { id, name: "opencodex" };
|
|
350
|
+
const entries = existing
|
|
351
|
+
? metadata.entries.map(current => current === existing ? entry : current)
|
|
352
|
+
: [...metadata.entries, entry];
|
|
353
|
+
|
|
354
|
+
const configJson = JSON.stringify(generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile), null, 2) + "\n";
|
|
355
|
+
const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
|
|
356
|
+
const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
|
|
357
|
+
try {
|
|
358
|
+
atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
|
|
359
|
+
} catch (metaError) {
|
|
360
|
+
// Rollback: restore the backed-up config if metadata write fails.
|
|
361
|
+
if (backupPath && existsSync(backupPath)) copyFileSync(backupPath, configPath);
|
|
362
|
+
throw metaError;
|
|
363
|
+
}
|
|
364
|
+
return { written: true, path: configPath, fingerprint };
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
367
|
+
return { written: false, path: configPath, reason };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Backup an existing owned config then atomically replace it. Exported for failure-path tests. */
|
|
372
|
+
export function atomicReplaceDesktopConfig(
|
|
373
|
+
path: string,
|
|
374
|
+
content: string,
|
|
375
|
+
writer: (path: string, content: string) => void = atomicWriteFile,
|
|
376
|
+
): { backupPath?: string } {
|
|
377
|
+
const backupPath = `${path}.bak`;
|
|
378
|
+
if (existsSync(path)) copyFileSync(path, backupPath);
|
|
379
|
+
writer(path, content);
|
|
380
|
+
return existsSync(backupPath) ? { backupPath } : {};
|
|
381
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** In-memory Desktop health tracker (resets on server restart). */
|
|
2
|
+
|
|
3
|
+
interface DesktopHealthState {
|
|
4
|
+
lastRequestAt: number | null;
|
|
5
|
+
requestCount: number;
|
|
6
|
+
errorCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const state: DesktopHealthState = { lastRequestAt: null, requestCount: 0, errorCount: 0 };
|
|
10
|
+
|
|
11
|
+
export function recordDesktopRequest(): void {
|
|
12
|
+
state.lastRequestAt = Date.now();
|
|
13
|
+
state.requestCount++;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function recordDesktopError(): void {
|
|
17
|
+
state.errorCount++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getDesktopHealth(): { lastRequestAt: string | null; requestCount: number; errorCount: number } {
|
|
21
|
+
return {
|
|
22
|
+
lastRequestAt: state.lastRequestAt !== null ? new Date(state.lastRequestAt).toISOString() : null,
|
|
23
|
+
requestCount: state.requestCount,
|
|
24
|
+
errorCount: state.errorCount,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
OcxClaudeDesktopAssignment,
|
|
4
|
+
OcxClaudeDesktopFamily,
|
|
5
|
+
OcxClaudeDesktopProfile,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
export const DESKTOP_FAMILIES = ["opus", "fable", "sonnet", "haiku"] as const;
|
|
9
|
+
export type DesktopFamily = OcxClaudeDesktopFamily;
|
|
10
|
+
export type DesktopProfile = OcxClaudeDesktopProfile;
|
|
11
|
+
|
|
12
|
+
export interface DesktopProfileModel {
|
|
13
|
+
route: string;
|
|
14
|
+
label: string;
|
|
15
|
+
contextWindow?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RenderedDesktopModel extends DesktopProfileModel {
|
|
19
|
+
name: string;
|
|
20
|
+
family: DesktopFamily;
|
|
21
|
+
isFamilyDefault: boolean;
|
|
22
|
+
supports1m: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DATE_ALIAS = /^claude-opus-4-8-(2026\d{4})$/;
|
|
26
|
+
const DAY_COUNT_2026 = 365;
|
|
27
|
+
|
|
28
|
+
export class DesktopProfileError extends Error {
|
|
29
|
+
constructor(message: string, readonly path = "profile") {
|
|
30
|
+
super(`${path}: ${message}`);
|
|
31
|
+
this.name = "DesktopProfileError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function emptyDesktopProfile(): DesktopProfile {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
assignments: {},
|
|
39
|
+
defaults: { opus: null, fable: null, sonnet: null, haiku: null },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertExactKeys(value: Record<string, unknown>, keys: readonly string[], path: string): void {
|
|
48
|
+
const allowed = new Set(keys);
|
|
49
|
+
for (const key of Object.keys(value)) {
|
|
50
|
+
if (!allowed.has(key)) throw new DesktopProfileError(`unknown field "${key}"`, path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Applied-state markers survive every profile rebuild.
|
|
56
|
+
*
|
|
57
|
+
* `parseDesktopProfile`, `reconcileDesktopProfile` and `moveDesktopRoute` each construct a
|
|
58
|
+
* fresh `{ version, assignments, defaults }`, and the management routes persist whatever they
|
|
59
|
+
* return. Without this carry-through, saving an assignment — or merely dragging a model to
|
|
60
|
+
* another family — would erase the fingerprint the apply route wrote, and the GUI would report
|
|
61
|
+
* "not applied" for a config that is applied on disk.
|
|
62
|
+
*/
|
|
63
|
+
function appliedMarkers(source: { appliedFingerprint?: unknown; appliedAt?: unknown }): {
|
|
64
|
+
appliedFingerprint?: string;
|
|
65
|
+
appliedAt?: string;
|
|
66
|
+
} {
|
|
67
|
+
return {
|
|
68
|
+
...(typeof source.appliedFingerprint === "string" ? { appliedFingerprint: source.appliedFingerprint } : {}),
|
|
69
|
+
...(typeof source.appliedAt === "string" ? { appliedAt: source.appliedAt } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isFamily(value: unknown): value is DesktopFamily {
|
|
74
|
+
return typeof value === "string" && (DESKTOP_FAMILIES as readonly string[]).includes(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function routeModelId(route: string): string {
|
|
78
|
+
const slash = route.indexOf("/");
|
|
79
|
+
return slash >= 0 ? route.slice(slash + 1) : route;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isRealAnthropicRoute(route: string): boolean {
|
|
83
|
+
return route.startsWith("anthropic/claude-");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validDateAlias(alias: string): boolean {
|
|
87
|
+
const match = DATE_ALIAS.exec(alias);
|
|
88
|
+
if (!match) return false;
|
|
89
|
+
const year = Number(match[1]!.slice(0, 4));
|
|
90
|
+
const month = Number(match[1]!.slice(4, 6));
|
|
91
|
+
const day = Number(match[1]!.slice(6, 8));
|
|
92
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
93
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseDesktopProfile(value: unknown): DesktopProfile {
|
|
97
|
+
if (!isPlainObject(value)) throw new DesktopProfileError("must be an object");
|
|
98
|
+
// `appliedFingerprint`/`appliedAt` are written back by the apply route once a profile
|
|
99
|
+
// reaches Claude Desktop (see server/management/agent-settings-routes.ts). Rejecting them
|
|
100
|
+
// here made every reload after the first apply fail with `unknown field`.
|
|
101
|
+
assertExactKeys(value, ["version", "assignments", "defaults", "appliedFingerprint", "appliedAt"], "profile");
|
|
102
|
+
if (value.version !== 1) throw new DesktopProfileError("version must be 1", "profile.version");
|
|
103
|
+
if (!isPlainObject(value.assignments)) throw new DesktopProfileError("must be an object", "profile.assignments");
|
|
104
|
+
if (!isPlainObject(value.defaults)) throw new DesktopProfileError("must be an object", "profile.defaults");
|
|
105
|
+
assertExactKeys(value.defaults, DESKTOP_FAMILIES, "profile.defaults");
|
|
106
|
+
if (value.appliedFingerprint !== undefined && typeof value.appliedFingerprint !== "string") {
|
|
107
|
+
throw new DesktopProfileError("must be a string", "profile.appliedFingerprint");
|
|
108
|
+
}
|
|
109
|
+
if (value.appliedAt !== undefined && typeof value.appliedAt !== "string") {
|
|
110
|
+
throw new DesktopProfileError("must be a string", "profile.appliedAt");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const assignments: Record<string, OcxClaudeDesktopAssignment> = {};
|
|
114
|
+
const aliases = new Set<string>();
|
|
115
|
+
for (const [route, raw] of Object.entries(value.assignments)) {
|
|
116
|
+
if (!route.trim() || !route.includes("/")) throw new DesktopProfileError("route must be provider/model", `profile.assignments.${route || "<empty>"}`);
|
|
117
|
+
if (!isPlainObject(raw)) throw new DesktopProfileError("must be an object", `profile.assignments.${route}`);
|
|
118
|
+
assertExactKeys(raw, ["family", "alias"], `profile.assignments.${route}`);
|
|
119
|
+
if (!isFamily(raw.family)) throw new DesktopProfileError("unknown family", `profile.assignments.${route}.family`);
|
|
120
|
+
if (typeof raw.alias !== "string" || !raw.alias) throw new DesktopProfileError("must be a non-empty string", `profile.assignments.${route}.alias`);
|
|
121
|
+
if (isRealAnthropicRoute(route)) {
|
|
122
|
+
if (raw.alias !== routeModelId(route)) throw new DesktopProfileError("real Anthropic routes must keep their exact model id", `profile.assignments.${route}.alias`);
|
|
123
|
+
} else if (!validDateAlias(raw.alias)) {
|
|
124
|
+
throw new DesktopProfileError("must be a valid claude-opus-4-8-2026MMDD alias", `profile.assignments.${route}.alias`);
|
|
125
|
+
}
|
|
126
|
+
if (aliases.has(raw.alias)) throw new DesktopProfileError(`duplicate alias "${raw.alias}"`, `profile.assignments.${route}.alias`);
|
|
127
|
+
aliases.add(raw.alias);
|
|
128
|
+
assignments[route] = { family: raw.family, alias: raw.alias };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const defaults = {} as DesktopProfile["defaults"];
|
|
132
|
+
for (const family of DESKTOP_FAMILIES) {
|
|
133
|
+
const route = value.defaults[family];
|
|
134
|
+
if (route !== null && typeof route !== "string") throw new DesktopProfileError("must be a route or null", `profile.defaults.${family}`);
|
|
135
|
+
const members = Object.keys(assignments).filter(key => assignments[key]!.family === family).sort();
|
|
136
|
+
if (members.length === 0) {
|
|
137
|
+
if (route !== null) throw new DesktopProfileError("must be null for an empty family", `profile.defaults.${family}`);
|
|
138
|
+
defaults[family] = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (typeof route !== "string" || !assignments[route] || assignments[route]!.family !== family) {
|
|
142
|
+
throw new DesktopProfileError("must reference a member of this family", `profile.defaults.${family}`);
|
|
143
|
+
}
|
|
144
|
+
defaults[family] = route;
|
|
145
|
+
}
|
|
146
|
+
return { version: 1, assignments, defaults, ...appliedMarkers(value) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function dayOfYearAlias(dayIndex: number): string {
|
|
150
|
+
const date = new Date(Date.UTC(2026, 0, dayIndex + 1));
|
|
151
|
+
const y = date.getUTCFullYear();
|
|
152
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
153
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
154
|
+
return `claude-opus-4-8-${y}${m}${d}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function routeStartDay(route: string): number {
|
|
158
|
+
return createHash("sha256").update(route).digest().readUInt32BE(0) % DAY_COUNT_2026;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function allocateAlias(route: string, used: Set<string>): string {
|
|
162
|
+
if (isRealAnthropicRoute(route)) return routeModelId(route);
|
|
163
|
+
const start = routeStartDay(route);
|
|
164
|
+
for (let offset = 0; offset < DAY_COUNT_2026; offset += 1) {
|
|
165
|
+
const alias = dayOfYearAlias((start + offset) % DAY_COUNT_2026);
|
|
166
|
+
if (!used.has(alias)) return alias;
|
|
167
|
+
}
|
|
168
|
+
throw new DesktopProfileError("all 365 encoded date slots are occupied", `profile.assignments.${route}.alias`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function reconcileDesktopProfile(
|
|
172
|
+
stored: unknown,
|
|
173
|
+
models: readonly DesktopProfileModel[],
|
|
174
|
+
): DesktopProfile {
|
|
175
|
+
const profile = stored === undefined || stored === null ? emptyDesktopProfile() : parseDesktopProfile(stored);
|
|
176
|
+
const assignments: DesktopProfile["assignments"] = Object.fromEntries(
|
|
177
|
+
Object.entries(profile.assignments).map(([route, assignment]) => [route, { ...assignment }]),
|
|
178
|
+
);
|
|
179
|
+
const used = new Set(Object.values(assignments).map(value => value.alias));
|
|
180
|
+
for (const model of [...models].sort((a, b) => a.route.localeCompare(b.route))) {
|
|
181
|
+
if (assignments[model.route]) continue;
|
|
182
|
+
const alias = allocateAlias(model.route, used);
|
|
183
|
+
used.add(alias);
|
|
184
|
+
assignments[model.route] = { family: "opus", alias };
|
|
185
|
+
}
|
|
186
|
+
const defaults = { ...profile.defaults };
|
|
187
|
+
for (const family of DESKTOP_FAMILIES) {
|
|
188
|
+
const members = Object.keys(assignments).filter(route => assignments[route]!.family === family).sort();
|
|
189
|
+
const current = defaults[family];
|
|
190
|
+
defaults[family] = current && assignments[current]?.family === family ? current : (members[0] ?? null);
|
|
191
|
+
}
|
|
192
|
+
return parseDesktopProfile({ version: 1, assignments, defaults, ...appliedMarkers(profile) });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function moveDesktopRoute(
|
|
196
|
+
profile: DesktopProfile,
|
|
197
|
+
route: string,
|
|
198
|
+
family: DesktopFamily,
|
|
199
|
+
makeDefault = false,
|
|
200
|
+
): DesktopProfile {
|
|
201
|
+
const parsed = parseDesktopProfile(profile);
|
|
202
|
+
const current = parsed.assignments[route];
|
|
203
|
+
if (!current) throw new DesktopProfileError("route is not assigned", `profile.assignments.${route}`);
|
|
204
|
+
const oldFamily = current.family;
|
|
205
|
+
if (oldFamily === family) {
|
|
206
|
+
return makeDefault ? setDesktopFamilyDefault(parsed, family, route) : parsed;
|
|
207
|
+
}
|
|
208
|
+
const assignments = { ...parsed.assignments, [route]: { ...current, family } };
|
|
209
|
+
const defaults = { ...parsed.defaults };
|
|
210
|
+
if (defaults[oldFamily] === route) {
|
|
211
|
+
defaults[oldFamily] = Object.keys(assignments).filter(key => key !== route && assignments[key]!.family === oldFamily).sort()[0] ?? null;
|
|
212
|
+
}
|
|
213
|
+
const destinationMembers = Object.keys(assignments).filter(key => assignments[key]!.family === family).sort();
|
|
214
|
+
if (makeDefault || !defaults[family] || assignments[defaults[family]!]?.family !== family) defaults[family] = route;
|
|
215
|
+
if (!defaults[family] && destinationMembers.length > 0) defaults[family] = destinationMembers[0]!;
|
|
216
|
+
return parseDesktopProfile({ version: 1, assignments, defaults, ...appliedMarkers(parsed) });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function setDesktopFamilyDefault(
|
|
220
|
+
profile: DesktopProfile,
|
|
221
|
+
family: DesktopFamily,
|
|
222
|
+
route: string | null,
|
|
223
|
+
): DesktopProfile {
|
|
224
|
+
const parsed = parseDesktopProfile(profile);
|
|
225
|
+
const members = Object.keys(parsed.assignments).filter(key => parsed.assignments[key]!.family === family);
|
|
226
|
+
if (route === null && members.length > 0) throw new DesktopProfileError("cannot clear a non-empty family default", `profile.defaults.${family}`);
|
|
227
|
+
if (route !== null && parsed.assignments[route]?.family !== family) throw new DesktopProfileError("route is not a member of this family", `profile.defaults.${family}`);
|
|
228
|
+
return parseDesktopProfile({ ...parsed, defaults: { ...parsed.defaults, [family]: route } });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function renderDesktopProfile(
|
|
232
|
+
profile: DesktopProfile,
|
|
233
|
+
models: readonly DesktopProfileModel[],
|
|
234
|
+
): RenderedDesktopModel[] {
|
|
235
|
+
const parsed = parseDesktopProfile(profile);
|
|
236
|
+
const modelByRoute = new Map(models.map(model => [model.route, model]));
|
|
237
|
+
const activeByFamily = new Map<DesktopFamily, string[]>();
|
|
238
|
+
for (const family of DESKTOP_FAMILIES) activeByFamily.set(family, []);
|
|
239
|
+
for (const [route, assignment] of Object.entries(parsed.assignments)) {
|
|
240
|
+
if (modelByRoute.has(route)) activeByFamily.get(assignment.family)!.push(route);
|
|
241
|
+
}
|
|
242
|
+
for (const routes of activeByFamily.values()) routes.sort();
|
|
243
|
+
const effectiveDefaults = {} as Record<DesktopFamily, string | null>;
|
|
244
|
+
for (const family of DESKTOP_FAMILIES) {
|
|
245
|
+
const active = activeByFamily.get(family)!;
|
|
246
|
+
const stored = parsed.defaults[family];
|
|
247
|
+
effectiveDefaults[family] = stored && active.includes(stored) ? stored : (active[0] ?? null);
|
|
248
|
+
}
|
|
249
|
+
const defaultOrder = DESKTOP_FAMILIES.map(family => effectiveDefaults[family]).filter((route): route is string => !!route);
|
|
250
|
+
const defaultSet = new Set(defaultOrder);
|
|
251
|
+
const rest = Object.keys(parsed.assignments).filter(route => modelByRoute.has(route) && !defaultSet.has(route)).sort();
|
|
252
|
+
return [...defaultOrder, ...rest].map(route => {
|
|
253
|
+
const model = modelByRoute.get(route)!;
|
|
254
|
+
const assignment = parsed.assignments[route]!;
|
|
255
|
+
return {
|
|
256
|
+
...model,
|
|
257
|
+
name: assignment.alias,
|
|
258
|
+
family: assignment.family,
|
|
259
|
+
isFamilyDefault: effectiveDefaults[assignment.family] === route,
|
|
260
|
+
supports1m: typeof model.contextWindow === "number" && model.contextWindow >= 1_000_000,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|