@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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight gate for CODEX_HOME storage mutations (cleanup, restore, policy).
|
|
3
|
+
*
|
|
4
|
+
* Manual cleanup, trash restore, and (Phase 3) policy-driven cleanup share one
|
|
5
|
+
* in-flight slot per resolved CODEX_HOME. A second caller receives
|
|
6
|
+
* `storage_mutation_busy` immediately — no per-operation queues.
|
|
7
|
+
*/
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
|
+
import { createAdmissionGate, type AdmissionLease, type AdmissionMetrics } from "../lib/admission";
|
|
11
|
+
|
|
12
|
+
export type StorageMutationKind = "cleanup" | "restore" | "policy";
|
|
13
|
+
|
|
14
|
+
export type StorageMutationBusyError = "storage_mutation_busy";
|
|
15
|
+
|
|
16
|
+
export interface StorageMutationCoordinatorTestHooks {
|
|
17
|
+
/** Block after acquiring the slot, before mutation work (race tests). */
|
|
18
|
+
blockMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ActiveSlot {
|
|
22
|
+
kind: StorageMutationKind;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
lease: AdmissionLease;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
|
|
28
|
+
const slots = new Map<string, ActiveSlot>();
|
|
29
|
+
const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
|
|
30
|
+
let releaseMisses = 0;
|
|
31
|
+
let testHooks: StorageMutationCoordinatorTestHooks | null = null;
|
|
32
|
+
|
|
33
|
+
function slotKey(codexHome?: string): string {
|
|
34
|
+
return resolve(codexHome ?? resolveCodexHomeDir());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setStorageMutationCoordinatorTestHooks(
|
|
38
|
+
hooks: StorageMutationCoordinatorTestHooks | null,
|
|
39
|
+
): void {
|
|
40
|
+
testHooks = hooks;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetStorageMutationCoordinatorForTests(): void {
|
|
44
|
+
testHooks = null;
|
|
45
|
+
for (const slot of slots.values()) slot.lease.release();
|
|
46
|
+
slots.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function storageMutationAdmissionMetrics(): AdmissionMetrics {
|
|
50
|
+
const metrics = slotGate.metrics();
|
|
51
|
+
return { ...metrics, releaseMisses: metrics.releaseMisses + releaseMisses };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getActiveStorageMutation(
|
|
55
|
+
codexHome?: string,
|
|
56
|
+
): { kind: StorageMutationKind; startedAt: number } | null {
|
|
57
|
+
const active = slots.get(slotKey(codexHome));
|
|
58
|
+
return active ? { kind: active.kind, startedAt: active.startedAt } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function tryBeginStorageMutation(
|
|
62
|
+
kind: StorageMutationKind,
|
|
63
|
+
codexHome?: string,
|
|
64
|
+
): { acquired: true; lease: AdmissionLease } | { acquired: false; error: StorageMutationBusyError } {
|
|
65
|
+
const key = slotKey(codexHome);
|
|
66
|
+
if (slots.has(key)) {
|
|
67
|
+
return { acquired: false, error: "storage_mutation_busy" };
|
|
68
|
+
}
|
|
69
|
+
const lease = slotGate.tryAcquire();
|
|
70
|
+
if (!lease) return { acquired: false, error: "storage_mutation_busy" };
|
|
71
|
+
let active = true;
|
|
72
|
+
const ownerLease: AdmissionLease = {
|
|
73
|
+
release() {
|
|
74
|
+
if (!active) return;
|
|
75
|
+
active = false;
|
|
76
|
+
const owner = slots.get(key);
|
|
77
|
+
if (owner?.lease === ownerLease) slots.delete(key);
|
|
78
|
+
lease.release();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
slots.set(key, { kind, startedAt: Date.now(), lease: ownerLease });
|
|
82
|
+
return { acquired: true, lease: ownerLease };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function endStorageMutation(codexHome?: string): void {
|
|
86
|
+
const key = slotKey(codexHome);
|
|
87
|
+
const slot = slots.get(key);
|
|
88
|
+
if (!slot) {
|
|
89
|
+
releaseMisses += 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
slots.delete(key);
|
|
93
|
+
slot.lease.release();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function applyCoordinatorBlock(): Promise<void> {
|
|
97
|
+
const blockMs = testHooks?.blockMs;
|
|
98
|
+
if (typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0) {
|
|
99
|
+
await Bun.sleep(Math.floor(blockMs));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Phase 3 policy worker integration — wrap policy-driven cleanup FS/DB work.
|
|
105
|
+
* Returns `{ ok: false, error: 'storage_mutation_busy' }` when another mutation
|
|
106
|
+
* holds the CODEX_HOME slot.
|
|
107
|
+
*/
|
|
108
|
+
export async function runPolicyStorageMutation<T>(
|
|
109
|
+
codexHome: string | undefined,
|
|
110
|
+
work: () => T | Promise<T>,
|
|
111
|
+
): Promise<T | { ok: false; error: StorageMutationBusyError }> {
|
|
112
|
+
const gate = tryBeginStorageMutation("policy", codexHome);
|
|
113
|
+
if (!gate.acquired) {
|
|
114
|
+
return { ok: false, error: "storage_mutation_busy" };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await applyCoordinatorBlock();
|
|
118
|
+
return await work();
|
|
119
|
+
} finally {
|
|
120
|
+
gate.lease.release();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function withStorageMutationSlot<T>(
|
|
125
|
+
kind: StorageMutationKind,
|
|
126
|
+
codexHome: string | undefined,
|
|
127
|
+
work: () => T | Promise<T>,
|
|
128
|
+
): Promise<T | { ok: false; error: StorageMutationBusyError }> {
|
|
129
|
+
const gate = tryBeginStorageMutation(kind, codexHome);
|
|
130
|
+
if (!gate.acquired) {
|
|
131
|
+
return { ok: false, error: "storage_mutation_busy" };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await applyCoordinatorBlock();
|
|
135
|
+
return await work();
|
|
136
|
+
} finally {
|
|
137
|
+
gate.lease.release();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic teardown for the storage Bun Workers.
|
|
3
|
+
*
|
|
4
|
+
* `Worker.terminate()` returns void and does NOT wait for the thread to be
|
|
5
|
+
* reclaimed. Callers that fire-and-forget leave a window where
|
|
6
|
+
* `bun test --isolate` reclaims the file realm while a Windows worker thread
|
|
7
|
+
* is still exiting — Bun then panics with
|
|
8
|
+
* `workers_spawned(N) workers_terminated(N-1)` / Internal assertion failure.
|
|
9
|
+
*
|
|
10
|
+
* Invariant: every registered worker has a `close` listener attached at spawn
|
|
11
|
+
* time (so we cannot miss `self.close()` / early exit), stays in `liveWorkers`
|
|
12
|
+
* until that close settles, and `drainStorageWorkers()` joins every in-flight
|
|
13
|
+
* terminate. Spawns are serialized through `withStorageWorkerSpawnGate` so the
|
|
14
|
+
* next Worker cannot be created until prior threads have exited. On Windows and
|
|
15
|
+
* macOS, a post-close settle covers the OS join gap Bun does not expose
|
|
16
|
+
* (Windows unbalanced join panic; macOS Silicon balanced-count segfault under
|
|
17
|
+
* `bun test --isolate`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
|
|
21
|
+
|
|
22
|
+
type TrackedWorker = {
|
|
23
|
+
worker: Worker;
|
|
24
|
+
closed: Promise<void>;
|
|
25
|
+
resolveClosed: () => void;
|
|
26
|
+
terminatePromise?: Promise<void>;
|
|
27
|
+
reservation?: AdmissionReservation<Worker>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const MAX_RESERVED_STORAGE_WORKER_SPAWNS = 16;
|
|
31
|
+
export class StorageWorkerAdmissionBusyError extends Error {
|
|
32
|
+
readonly code = "storage_mutation_busy";
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
super("storage worker spawn queue is busy");
|
|
36
|
+
this.name = "StorageWorkerAdmissionBusyError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const liveWorkers = new Map<Worker, TrackedWorker>();
|
|
41
|
+
const workerGate = createAdmissionGate("storage_worker_reservations", MAX_RESERVED_STORAGE_WORKER_SPAWNS);
|
|
42
|
+
|
|
43
|
+
/** Serialize spawns so a new Worker never overlaps a still-exiting predecessor. */
|
|
44
|
+
let spawnGate: Promise<void> = Promise.resolve();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bumped by teardown so a spawn still queued on `spawnGate` (not yet in
|
|
48
|
+
* `liveWorkers`) cannot create a Worker after reset/shutdown reported idle.
|
|
49
|
+
*/
|
|
50
|
+
let spawnCancelEpoch = 0;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* OS-join gap after the `close` event on platforms where Bun's Worker reclaim
|
|
54
|
+
* races the isolate/file boundary (not a CI job-timeout bump).
|
|
55
|
+
* Windows GHA at 250ms and 750ms still left `workers_spawned(N)
|
|
56
|
+
* workers_terminated(N-1)` panics under isolate (seen mid
|
|
57
|
+
* `storage-mutation-race` with 11/10). 1500ms covers deferred reclaim under
|
|
58
|
+
* stacked policy/restore workers. Darwin uses 250ms.
|
|
59
|
+
*/
|
|
60
|
+
const WORKER_OS_JOIN_MS = process.platform === "win32" ? 1_500 : 250;
|
|
61
|
+
|
|
62
|
+
function needsWorkerOsJoinSettle(): boolean {
|
|
63
|
+
return process.platform === "win32" || process.platform === "darwin";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Invalidate spawn callbacks still waiting on the gate (reset / server drain). */
|
|
67
|
+
export function cancelQueuedStorageWorkerSpawns(): void {
|
|
68
|
+
spawnCancelEpoch += 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Track a freshly spawned worker so teardown can wait for it later. */
|
|
72
|
+
export function registerStorageWorker(worker: Worker): void {
|
|
73
|
+
if (liveWorkers.has(worker)) return;
|
|
74
|
+
|
|
75
|
+
let resolveClosed!: () => void;
|
|
76
|
+
const closed = new Promise<void>(resolve => {
|
|
77
|
+
resolveClosed = resolve;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const tracked: TrackedWorker = { worker, closed, resolveClosed };
|
|
81
|
+
liveWorkers.set(worker, tracked);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
worker.addEventListener("close", () => {
|
|
85
|
+
resolveClosed();
|
|
86
|
+
}, { once: true });
|
|
87
|
+
} catch {
|
|
88
|
+
// No close event — terminate path will still resolve via timeout/settle.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function tryReserveStorageWorker(): AdmissionReservation<Worker> | null {
|
|
93
|
+
const gateLease = workerGate.tryAcquire();
|
|
94
|
+
if (!gateLease) return null;
|
|
95
|
+
let active = true;
|
|
96
|
+
let bound: Worker | undefined;
|
|
97
|
+
const reservation: AdmissionReservation<Worker> = {
|
|
98
|
+
bind(worker) {
|
|
99
|
+
if (!active) return;
|
|
100
|
+
bound = worker;
|
|
101
|
+
registerStorageWorker(worker);
|
|
102
|
+
const tracked = liveWorkers.get(worker);
|
|
103
|
+
if (tracked) tracked.reservation = reservation;
|
|
104
|
+
},
|
|
105
|
+
release() {
|
|
106
|
+
if (!active) return;
|
|
107
|
+
active = false;
|
|
108
|
+
if (bound) {
|
|
109
|
+
const tracked = liveWorkers.get(bound);
|
|
110
|
+
if (tracked?.reservation === reservation) tracked.reservation = undefined;
|
|
111
|
+
}
|
|
112
|
+
bound = undefined;
|
|
113
|
+
gateLease.release();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
return reservation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function storageWorkerAdmissionMetrics(): AdmissionMetrics {
|
|
120
|
+
return workerGate.metrics();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run `fn` only after every previously gated spawn has finished tearing down.
|
|
125
|
+
* Used around `new Worker(...)` so tests cannot overlap Windows thread exit.
|
|
126
|
+
*/
|
|
127
|
+
export function withStorageWorkerSpawnGate<T>(fn: () => Promise<T>): Promise<T> {
|
|
128
|
+
const epochAtEnqueue = spawnCancelEpoch;
|
|
129
|
+
const run = spawnGate.then(async () => {
|
|
130
|
+
if (epochAtEnqueue !== spawnCancelEpoch) {
|
|
131
|
+
throw new Error("storage_worker_spawn_cancelled");
|
|
132
|
+
}
|
|
133
|
+
await drainStorageWorkers();
|
|
134
|
+
if (epochAtEnqueue !== spawnCancelEpoch) {
|
|
135
|
+
throw new Error("storage_worker_spawn_cancelled");
|
|
136
|
+
}
|
|
137
|
+
return fn();
|
|
138
|
+
});
|
|
139
|
+
spawnGate = run.then(
|
|
140
|
+
() => undefined,
|
|
141
|
+
() => undefined,
|
|
142
|
+
);
|
|
143
|
+
return run;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function queuedStorageWorkerSpawnCount(): number {
|
|
147
|
+
return Math.max(0, workerGate.metrics().active - liveWorkers.size);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Terminate a worker and resolve once its thread has actually exited.
|
|
152
|
+
*
|
|
153
|
+
* Safe to call twice: the second call joins the in-flight terminate promise.
|
|
154
|
+
*/
|
|
155
|
+
export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promise<void> {
|
|
156
|
+
const tracked = liveWorkers.get(worker);
|
|
157
|
+
if (!tracked) {
|
|
158
|
+
try { worker.terminate(); } catch { /* already gone */ }
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
if (tracked.terminatePromise) return tracked.terminatePromise;
|
|
162
|
+
|
|
163
|
+
tracked.terminatePromise = (async () => {
|
|
164
|
+
let timedOut = false;
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
tracked.resolveClosed();
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
try {
|
|
172
|
+
worker.terminate();
|
|
173
|
+
} catch {
|
|
174
|
+
tracked.resolveClosed();
|
|
175
|
+
}
|
|
176
|
+
await tracked.closed;
|
|
177
|
+
// Disarm before the OS-join settle: a late timer firing during the sleep
|
|
178
|
+
// would set timedOut after close already won and throw a false timeout.
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
// Always run the OS-join settle before throwing on timeout: the timer
|
|
181
|
+
// only forces `closed`, it does not prove the OS thread has exited.
|
|
182
|
+
// Callers that catch and continue (e.g. drainAndShutdown) still need
|
|
183
|
+
// that gap before the next isolate reclaim or server.stop.
|
|
184
|
+
if (needsWorkerOsJoinSettle()) {
|
|
185
|
+
await Bun.sleep(0);
|
|
186
|
+
await Bun.sleep(WORKER_OS_JOIN_MS);
|
|
187
|
+
}
|
|
188
|
+
if (timedOut) {
|
|
189
|
+
throw new Error(`storage worker did not exit within ${timeoutMs}ms`);
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
liveWorkers.delete(worker);
|
|
194
|
+
tracked.reservation?.release();
|
|
195
|
+
tracked.reservation = undefined;
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
return tracked.terminatePromise;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Await every worker this module still tracks (including terminations already
|
|
204
|
+
* in flight). Used by test resets so no storage worker outlives the file that
|
|
205
|
+
* spawned it.
|
|
206
|
+
*/
|
|
207
|
+
export async function drainStorageWorkers(timeoutMs = 5_000): Promise<void> {
|
|
208
|
+
const pending = [...liveWorkers.keys()].map(worker => terminateStorageWorker(worker, timeoutMs));
|
|
209
|
+
await Promise.all(pending);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Live worker count — exported so a regression test can assert the invariant. */
|
|
213
|
+
export function liveStorageWorkerCount(): number {
|
|
214
|
+
return liveWorkers.size;
|
|
215
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][string]$BunPath,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$CliPath,
|
|
4
|
+
[Parameter(Mandatory = $true)][string]$CodexHome,
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$OpenCodexHome,
|
|
6
|
+
[ValidateSet("Run", "Stop")][string]$Mode = "Run",
|
|
7
|
+
[int]$HostPid = 0
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = "Stop"
|
|
11
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
12
|
+
Add-Type -AssemblyName System.Drawing
|
|
13
|
+
|
|
14
|
+
# Normalize aliases before deriving singleton/event names. Without this,
|
|
15
|
+
# C:\path and C:\path\. create separate tray instances for the same home.
|
|
16
|
+
function Normalize-HomePath([string]$Value) {
|
|
17
|
+
$full = [System.IO.Path]::GetFullPath($Value)
|
|
18
|
+
$root = [System.IO.Path]::GetPathRoot($full)
|
|
19
|
+
if ($full -eq $root) { return $full }
|
|
20
|
+
return $full.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
|
|
21
|
+
}
|
|
22
|
+
$OpenCodexHome = Normalize-HomePath $OpenCodexHome
|
|
23
|
+
$CodexHome = Normalize-HomePath $CodexHome
|
|
24
|
+
|
|
25
|
+
$script:ownedIcons = New-Object System.Collections.Generic.List[System.Drawing.Icon]
|
|
26
|
+
function Load-TrayIcon([string]$Name, [System.Drawing.Icon]$Fallback) {
|
|
27
|
+
$path = Join-Path $OpenCodexHome $Name
|
|
28
|
+
if (-not [System.IO.File]::Exists($path)) { return $Fallback }
|
|
29
|
+
try {
|
|
30
|
+
$icon = New-Object System.Drawing.Icon($path)
|
|
31
|
+
[void]$script:ownedIcons.Add($icon)
|
|
32
|
+
return $icon
|
|
33
|
+
} catch {
|
|
34
|
+
return $Fallback
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
$onlineIcon = Load-TrayIcon "opencodex-tray-online.ico" ([System.Drawing.SystemIcons]::Information)
|
|
38
|
+
$warningIcon = Load-TrayIcon "opencodex-tray-warning.ico" ([System.Drawing.SystemIcons]::Warning)
|
|
39
|
+
$offlineIcon = Load-TrayIcon "opencodex-tray-offline.ico" ([System.Drawing.SystemIcons]::Error)
|
|
40
|
+
|
|
41
|
+
function Get-StableHash([string]$Value) {
|
|
42
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
43
|
+
try {
|
|
44
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value.ToLowerInvariant())
|
|
45
|
+
$hash = [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace("-", "").Substring(0, 20)
|
|
46
|
+
return $hash
|
|
47
|
+
} finally {
|
|
48
|
+
$sha.Dispose()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$stableHash = Get-StableHash $OpenCodexHome
|
|
53
|
+
$stopEventCreated = $false
|
|
54
|
+
$stopEvent = New-Object System.Threading.EventWaitHandle($false, [System.Threading.EventResetMode]::AutoReset, "Local\OpenCodexTrayStop-$stableHash", [ref]$stopEventCreated)
|
|
55
|
+
if ($Mode -eq "Stop") {
|
|
56
|
+
[void]$stopEvent.Set()
|
|
57
|
+
$stopEvent.Dispose()
|
|
58
|
+
exit 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$createdNew = $false
|
|
62
|
+
$mutex = New-Object System.Threading.Mutex($true, "Local\OpenCodexTray-$stableHash", [ref]$createdNew)
|
|
63
|
+
if (-not $createdNew) {
|
|
64
|
+
$stopEvent.Dispose()
|
|
65
|
+
$mutex.Dispose()
|
|
66
|
+
exit 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$heartbeatPath = Join-Path $OpenCodexHome "tray-heartbeat.json"
|
|
70
|
+
$actionLogPath = Join-Path $OpenCodexHome "tray-actions.log"
|
|
71
|
+
|
|
72
|
+
function Write-ActionLog([string]$Message) {
|
|
73
|
+
$line = "[$([DateTimeOffset]::Now.ToString('o'))] $Message"
|
|
74
|
+
[System.IO.File]::AppendAllText($actionLogPath, $line + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ConvertTo-NativeArgument([string]$Value) {
|
|
78
|
+
if ($Value.Contains('"') -or $Value.Contains("`r") -or $Value.Contains("`n")) {
|
|
79
|
+
throw "Invalid native command argument"
|
|
80
|
+
}
|
|
81
|
+
return '"' + $Value + '"'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Start-OcxCommand([string[]]$CommandArgs) {
|
|
85
|
+
try {
|
|
86
|
+
$allArgs = @($CliPath) + $CommandArgs
|
|
87
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
88
|
+
$psi.FileName = $BunPath
|
|
89
|
+
$psi.Arguments = (($allArgs | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ")
|
|
90
|
+
$psi.UseShellExecute = $false
|
|
91
|
+
$psi.CreateNoWindow = $true
|
|
92
|
+
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
93
|
+
$psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
|
|
94
|
+
$psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
95
|
+
$process = [System.Diagnostics.Process]::Start($psi)
|
|
96
|
+
if ($null -ne $process) { $process.Dispose() }
|
|
97
|
+
Write-ActionLog "dispatched $($CommandArgs -join ' ')"
|
|
98
|
+
return $true
|
|
99
|
+
} catch {
|
|
100
|
+
Write-ActionLog "launch failed: $($_.Exception.GetType().Name)"
|
|
101
|
+
$notify.ShowBalloonTip(5000, "opencodex action failed", "The action could not start. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
102
|
+
return $false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Read-ListenTarget {
|
|
107
|
+
foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) {
|
|
108
|
+
try {
|
|
109
|
+
$value = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
|
|
110
|
+
$candidate = [int]$value.port
|
|
111
|
+
if ($candidate -gt 0 -and $candidate -le 65535) {
|
|
112
|
+
$candidateHost = [string]$value.hostname
|
|
113
|
+
$ip = $null
|
|
114
|
+
$hostName = if ([string]::IsNullOrWhiteSpace($candidateHost) -or $candidateHost -in @("localhost", "0.0.0.0", "::", "[::]")) {
|
|
115
|
+
"127.0.0.1"
|
|
116
|
+
} elseif ([System.Net.IPAddress]::TryParse($candidateHost.Trim("[", "]"), [ref]$ip)) {
|
|
117
|
+
if ($ip.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) { "[$($ip.ToString())]" } else { $ip.ToString() }
|
|
118
|
+
} else {
|
|
119
|
+
"127.0.0.1"
|
|
120
|
+
}
|
|
121
|
+
return @{ port = $candidate; host = $hostName; pid = $value.pid }
|
|
122
|
+
}
|
|
123
|
+
} catch { }
|
|
124
|
+
}
|
|
125
|
+
return @{ port = 10100; host = "127.0.0.1"; pid = $null }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function Read-JsonUrl([string]$Url) {
|
|
129
|
+
$request = [System.Net.HttpWebRequest]::Create($Url)
|
|
130
|
+
$request.Method = "GET"
|
|
131
|
+
$request.Timeout = 700
|
|
132
|
+
$request.ReadWriteTimeout = 700
|
|
133
|
+
$response = $request.GetResponse()
|
|
134
|
+
try {
|
|
135
|
+
$reader = New-Object System.IO.StreamReader($response.GetResponseStream())
|
|
136
|
+
try { return ($reader.ReadToEnd() | ConvertFrom-Json) } finally { $reader.Dispose() }
|
|
137
|
+
} finally {
|
|
138
|
+
$response.Dispose()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
$notify = New-Object System.Windows.Forms.NotifyIcon
|
|
143
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
144
|
+
$statusItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
145
|
+
$statusItem.Enabled = $false
|
|
146
|
+
$safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
147
|
+
$safetyItem.Enabled = $false
|
|
148
|
+
$openItem = $menu.Items.Add("Open Dashboard")
|
|
149
|
+
$startItem = $menu.Items.Add("Start Proxy")
|
|
150
|
+
$stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing")
|
|
151
|
+
$restartItem = $menu.Items.Add("Restart Proxy")
|
|
152
|
+
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
|
|
153
|
+
[void]$menu.Items.Add($statusItem)
|
|
154
|
+
[void]$menu.Items.Add($safetyItem)
|
|
155
|
+
$logsItem = $menu.Items.Add("Open Logs Folder")
|
|
156
|
+
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
|
|
157
|
+
$exitItem = $menu.Items.Add("Exit Tray")
|
|
158
|
+
|
|
159
|
+
$script:online = $false
|
|
160
|
+
$script:port = 10100
|
|
161
|
+
$script:proxyPid = $null
|
|
162
|
+
$script:pendingAction = $null
|
|
163
|
+
$script:pendingStarted = 0L
|
|
164
|
+
$script:pendingDeadline = 0L
|
|
165
|
+
$script:pendingOldProxyPid = $null
|
|
166
|
+
|
|
167
|
+
function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) {
|
|
168
|
+
$script:pendingAction = $Action
|
|
169
|
+
$script:pendingStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
170
|
+
$script:pendingDeadline = $script:pendingStarted + ($TimeoutSeconds * 1000)
|
|
171
|
+
$script:pendingOldProxyPid = $script:proxyPid
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Complete-PendingAction([bool]$Success) {
|
|
175
|
+
if ($null -eq $script:pendingAction) { return }
|
|
176
|
+
$action = $script:pendingAction
|
|
177
|
+
$script:pendingAction = $null
|
|
178
|
+
if ($Success) {
|
|
179
|
+
Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))"
|
|
180
|
+
$notify.ShowBalloonTip(2500, "opencodex", "$action completed.", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
181
|
+
} else {
|
|
182
|
+
Write-ActionLog "$action failed to reach the expected state"
|
|
183
|
+
$notify.ShowBalloonTip(5000, "opencodex action failed", "$action did not reach the expected state. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Update-TrayState {
|
|
188
|
+
$target = Read-ListenTarget
|
|
189
|
+
$script:port = [int]$target.port
|
|
190
|
+
$health = $null
|
|
191
|
+
$origin = "http://$($target.host):$($script:port)"
|
|
192
|
+
try { $health = Read-JsonUrl "$origin/healthz" } catch { }
|
|
193
|
+
$pidMatches = $null -eq $target.pid -or [int]$target.pid -eq [int]$health.pid
|
|
194
|
+
$script:online = $null -ne $health -and $health.status -eq "ok" -and $health.service -eq "opencodex" -and [int]$health.port -eq $script:port -and $pidMatches
|
|
195
|
+
$script:proxyPid = if ($script:online) { [int]$health.pid } else { $null }
|
|
196
|
+
if ($script:online) {
|
|
197
|
+
$statusItem.Text = "Proxy: Online (port $($script:port))"
|
|
198
|
+
$notify.Text = "opencodex: Online"
|
|
199
|
+
$startItem.Enabled = $false
|
|
200
|
+
$stopItem.Enabled = $true
|
|
201
|
+
$restartItem.Enabled = $true
|
|
202
|
+
try {
|
|
203
|
+
$startup = Read-JsonUrl "$origin/api/startup-health"
|
|
204
|
+
$label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
|
|
205
|
+
$safetyItem.Text = "Restart safety: $label"
|
|
206
|
+
$notify.Icon = if ($startup.status -eq "at-risk") { $warningIcon } else { $onlineIcon }
|
|
207
|
+
} catch {
|
|
208
|
+
$safetyItem.Text = "Restart safety: unavailable"
|
|
209
|
+
$notify.Icon = $warningIcon
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
$statusItem.Text = "Proxy: Offline"
|
|
213
|
+
$safetyItem.Text = "Restart safety: start the proxy to inspect"
|
|
214
|
+
$notify.Text = "opencodex: Offline"
|
|
215
|
+
$notify.Icon = $offlineIcon
|
|
216
|
+
$startItem.Enabled = $true
|
|
217
|
+
$stopItem.Enabled = $false
|
|
218
|
+
$restartItem.Enabled = $false
|
|
219
|
+
}
|
|
220
|
+
$heartbeat = @{ pid = $PID; timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
|
|
221
|
+
if ($HostPid -gt 0) { $heartbeat.hostPid = $HostPid }
|
|
222
|
+
$heartbeatJson = $heartbeat | ConvertTo-Json -Compress
|
|
223
|
+
[System.IO.File]::WriteAllText($heartbeatPath, $heartbeatJson, (New-Object System.Text.UTF8Encoding($false)))
|
|
224
|
+
|
|
225
|
+
if ($null -ne $script:pendingAction) {
|
|
226
|
+
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
227
|
+
$elapsed = $now - $script:pendingStarted
|
|
228
|
+
$reached = ($script:pendingAction -eq "Start Proxy" -and $script:online) -or
|
|
229
|
+
($script:pendingAction -eq "Stop Proxy" -and -not $script:online) -or
|
|
230
|
+
($script:pendingAction -eq "Restart Proxy" -and $elapsed -gt 3000 -and $script:online -and $script:proxyPid -ne $script:pendingOldProxyPid)
|
|
231
|
+
if ($reached) { Complete-PendingAction $true }
|
|
232
|
+
elseif ($now -gt $script:pendingDeadline) { Complete-PendingAction $false }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
$openItem.add_Click({ Start-OcxCommand @("gui") })
|
|
237
|
+
$startItem.add_Click({
|
|
238
|
+
$statusItem.Text = "Proxy: Starting..."
|
|
239
|
+
Set-PendingAction "Start Proxy" 15
|
|
240
|
+
if (-not (Start-OcxCommand @("__tray-start"))) { $script:pendingAction = $null }
|
|
241
|
+
})
|
|
242
|
+
$stopItem.add_Click({
|
|
243
|
+
$statusItem.Text = "Proxy: Stopping..."
|
|
244
|
+
Set-PendingAction "Stop Proxy" 15
|
|
245
|
+
if (-not (Start-OcxCommand @("stop"))) { $script:pendingAction = $null }
|
|
246
|
+
})
|
|
247
|
+
$restartItem.add_Click({
|
|
248
|
+
$statusItem.Text = "Proxy: Restarting..."
|
|
249
|
+
Set-PendingAction "Restart Proxy" 20
|
|
250
|
+
if (-not (Start-OcxCommand @("__tray-restart"))) { $script:pendingAction = $null }
|
|
251
|
+
})
|
|
252
|
+
$logsItem.add_Click({
|
|
253
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
254
|
+
$psi.FileName = $OpenCodexHome
|
|
255
|
+
$psi.UseShellExecute = $true
|
|
256
|
+
[void][System.Diagnostics.Process]::Start($psi)
|
|
257
|
+
})
|
|
258
|
+
$exitItem.add_Click({ [System.Windows.Forms.Application]::Exit() })
|
|
259
|
+
$notify.add_DoubleClick({ Start-OcxCommand @("gui") })
|
|
260
|
+
|
|
261
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
262
|
+
$timer.Interval = 3000
|
|
263
|
+
$timer.add_Tick({
|
|
264
|
+
if ($stopEvent.WaitOne(0)) {
|
|
265
|
+
[System.Windows.Forms.Application]::Exit()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
Update-TrayState
|
|
269
|
+
})
|
|
270
|
+
$notify.ContextMenuStrip = $menu
|
|
271
|
+
$notify.Icon = $offlineIcon
|
|
272
|
+
$notify.Visible = $true
|
|
273
|
+
$notify.Text = "opencodex: Checking..."
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
Update-TrayState
|
|
277
|
+
$timer.Start()
|
|
278
|
+
[System.Windows.Forms.Application]::Run()
|
|
279
|
+
} finally {
|
|
280
|
+
$timer.Stop()
|
|
281
|
+
$timer.Dispose()
|
|
282
|
+
$notify.Visible = $false
|
|
283
|
+
$notify.Dispose()
|
|
284
|
+
foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
|
|
285
|
+
$menu.Dispose()
|
|
286
|
+
try { Remove-Item -LiteralPath $heartbeatPath -Force -ErrorAction SilentlyContinue } catch { }
|
|
287
|
+
try { $mutex.ReleaseMutex() } catch { }
|
|
288
|
+
$mutex.Dispose()
|
|
289
|
+
$stopEvent.Dispose()
|
|
290
|
+
}
|