@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory watchdog (#314 WP3 / #509) — warn-only observability for the Windows
|
|
3
|
+
* native-memory growth reported upstream (Bun fetch buffers / socket handles).
|
|
4
|
+
*
|
|
5
|
+
* Samples process.memoryUsage() on an unref'd interval into a bounded ring and
|
|
6
|
+
* logs ONE rate-limited warning when observed memory crosses the threshold. It never
|
|
7
|
+
* restarts anything (threshold auto-restart is deliberately deferred; the
|
|
8
|
+
* service managers' crash-respawn already covers hard failures). The active
|
|
9
|
+
* instance is a module-level singleton so the management API can expose the
|
|
10
|
+
* snapshot without threading server state through route contexts.
|
|
11
|
+
*
|
|
12
|
+
* Privacy: samples are scalar numbers only; the warn line never interpolates
|
|
13
|
+
* paths, hostnames, or tokens.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type MemorySampleBase = {
|
|
17
|
+
/** Epoch ms. */
|
|
18
|
+
at: number;
|
|
19
|
+
/** Resident set size in bytes. */
|
|
20
|
+
rss: number;
|
|
21
|
+
/** JS heap used in bytes (process.memoryUsage().heapUsed). */
|
|
22
|
+
heapUsed: number;
|
|
23
|
+
/** JS heap total in bytes. */
|
|
24
|
+
heapTotal: number;
|
|
25
|
+
/** External/native memory tracked by process.memoryUsage(). */
|
|
26
|
+
external: number;
|
|
27
|
+
/** ArrayBuffer memory tracked by process.memoryUsage(). */
|
|
28
|
+
arrayBuffers: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type MemorySample = MemorySampleBase & {
|
|
32
|
+
/** Largest observed memory counter used for thresholding. */
|
|
33
|
+
observedBytes: number;
|
|
34
|
+
/** Counter that produced observedBytes. */
|
|
35
|
+
observedMetric: MemoryMetric;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemoryMetric = "rss" | "external" | "arrayBuffers";
|
|
39
|
+
|
|
40
|
+
export type MemoryWatchdogState = {
|
|
41
|
+
samples: MemorySample[];
|
|
42
|
+
warnThresholdBytes: number;
|
|
43
|
+
lastWarnAt: number | null;
|
|
44
|
+
observedBytes: number;
|
|
45
|
+
observedMetric: MemoryMetric;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MemoryWatchdog = {
|
|
49
|
+
stop(): void;
|
|
50
|
+
snapshot(): MemoryWatchdogState;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
54
|
+
const DEFAULT_WARN_THRESHOLD_BYTES = 4 * 1024 ** 3; // 4 GiB
|
|
55
|
+
const DEFAULT_RING_SIZE = 360; // ≈6h at 60s
|
|
56
|
+
const WARN_INTERVAL_MS = 30 * 60_000;
|
|
57
|
+
const DOCS_URL = "https://opencodex.me/troubleshooting/windows-memory/";
|
|
58
|
+
|
|
59
|
+
let active: MemoryWatchdog | null = null;
|
|
60
|
+
|
|
61
|
+
export function observedMemoryCounter(sample: Pick<MemorySampleBase, "rss" | "external" | "arrayBuffers">): {
|
|
62
|
+
observedBytes: number;
|
|
63
|
+
observedMetric: MemoryMetric;
|
|
64
|
+
} {
|
|
65
|
+
const values: Array<{ metric: MemoryMetric; bytes: number }> = [
|
|
66
|
+
{ metric: "rss", bytes: sample.rss },
|
|
67
|
+
{ metric: "external", bytes: sample.external },
|
|
68
|
+
{ metric: "arrayBuffers", bytes: sample.arrayBuffers },
|
|
69
|
+
];
|
|
70
|
+
const best = values.reduce((current, next) => next.bytes > current.bytes ? next : current, values[0]);
|
|
71
|
+
return { observedBytes: best.bytes, observedMetric: best.metric };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The running watchdog, if any — read by /api/system/memory. */
|
|
75
|
+
export function getActiveMemoryWatchdog(): MemoryWatchdog | null {
|
|
76
|
+
return active;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function defaultSample(now: () => number): MemorySample {
|
|
80
|
+
const usage = process.memoryUsage();
|
|
81
|
+
const base = {
|
|
82
|
+
at: now(),
|
|
83
|
+
rss: usage.rss,
|
|
84
|
+
heapUsed: usage.heapUsed,
|
|
85
|
+
heapTotal: usage.heapTotal,
|
|
86
|
+
external: usage.external,
|
|
87
|
+
arrayBuffers: usage.arrayBuffers,
|
|
88
|
+
};
|
|
89
|
+
return { ...base, ...observedMemoryCounter(base) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeSample(sample: MemorySampleBase): MemorySample {
|
|
93
|
+
return { ...sample, ...observedMemoryCounter(sample) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start (or replace) the process-wide memory watchdog. Idempotent: a previous
|
|
98
|
+
* active instance is stopped first, so repeated startServer() calls in tests
|
|
99
|
+
* never accumulate intervals. The timer is unref'd; stop() is exposed for
|
|
100
|
+
* tests and clears the singleton.
|
|
101
|
+
*/
|
|
102
|
+
export function startMemoryWatchdog(opts?: {
|
|
103
|
+
intervalMs?: number;
|
|
104
|
+
warnThresholdBytes?: number;
|
|
105
|
+
ringSize?: number;
|
|
106
|
+
now?: () => number;
|
|
107
|
+
sample?: () => MemorySampleBase;
|
|
108
|
+
warn?: (msg: string) => void;
|
|
109
|
+
}): MemoryWatchdog {
|
|
110
|
+
active?.stop();
|
|
111
|
+
const intervalMs = opts?.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
112
|
+
const warnThresholdBytes = opts?.warnThresholdBytes ?? DEFAULT_WARN_THRESHOLD_BYTES;
|
|
113
|
+
const ringSize = opts?.ringSize ?? DEFAULT_RING_SIZE;
|
|
114
|
+
const now = opts?.now ?? Date.now;
|
|
115
|
+
const sample = opts?.sample ?? (() => defaultSample(now));
|
|
116
|
+
const warn = opts?.warn ?? ((msg: string) => console.warn(msg));
|
|
117
|
+
|
|
118
|
+
const samples: MemorySample[] = [];
|
|
119
|
+
let lastWarnAt: number | null = null;
|
|
120
|
+
let observedBytes = 0;
|
|
121
|
+
let observedMetric: MemoryMetric = "rss";
|
|
122
|
+
|
|
123
|
+
const tick = () => {
|
|
124
|
+
let s: MemorySample;
|
|
125
|
+
try {
|
|
126
|
+
s = normalizeSample(sample());
|
|
127
|
+
} catch {
|
|
128
|
+
return; // sampling must never break the server
|
|
129
|
+
}
|
|
130
|
+
samples.push(s);
|
|
131
|
+
if (samples.length > ringSize) samples.splice(0, samples.length - ringSize);
|
|
132
|
+
observedBytes = s.observedBytes;
|
|
133
|
+
observedMetric = s.observedMetric;
|
|
134
|
+
if (s.observedBytes >= warnThresholdBytes && (lastWarnAt === null || now() - lastWarnAt >= WARN_INTERVAL_MS)) {
|
|
135
|
+
lastWarnAt = now();
|
|
136
|
+
const observedMb = Math.round(s.observedBytes / (1024 * 1024));
|
|
137
|
+
const thresholdMb = Math.round(warnThresholdBytes / (1024 * 1024));
|
|
138
|
+
warn(`⚠️ opencodex observed memory ${observedMb}MB (${s.observedMetric}) exceeds the ${thresholdMb}MB watch threshold. On Windows this is usually the upstream Bun runtime memory issue — see ${DOCS_URL}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const timer = setInterval(tick, intervalMs);
|
|
143
|
+
(timer as { unref?: () => void }).unref?.();
|
|
144
|
+
|
|
145
|
+
const instance: MemoryWatchdog = {
|
|
146
|
+
stop() {
|
|
147
|
+
clearInterval(timer);
|
|
148
|
+
if (active === instance) active = null;
|
|
149
|
+
},
|
|
150
|
+
snapshot() {
|
|
151
|
+
return { samples: [...samples], warnThresholdBytes, lastWarnAt, observedBytes, observedMetric };
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
active = instance;
|
|
155
|
+
return instance;
|
|
156
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reclaim a listen port after stop/update so restart can stay on the configured
|
|
3
|
+
* port instead of hopping to an ephemeral one (Windows CLOSE_WAIT / leftover ocx).
|
|
4
|
+
*
|
|
5
|
+
* Killing is never the default. A process may be killed only when the caller
|
|
6
|
+
* sets `killOcxHolders` and either supplies a non-empty `onlyKillPids` allowlist
|
|
7
|
+
* (trusted teardown PIDs, including allowlisted holders that fail ocx revalidate)
|
|
8
|
+
* or enables `killAllOcxOnPort` for revalidated ocx listeners. Unknown foreign
|
|
9
|
+
* (non-ocx, non-allowlisted) processes are never killed.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { verifyPidIdentity } from "../config";
|
|
13
|
+
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
14
|
+
import { isPortAvailable, type WaitForPortOptions } from "./ports";
|
|
15
|
+
import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
|
|
16
|
+
|
|
17
|
+
export type ListenPidScan =
|
|
18
|
+
| { ok: true; pids: number[] }
|
|
19
|
+
| { ok: false; error?: string };
|
|
20
|
+
|
|
21
|
+
export type ReclaimListenPortOptions = WaitForPortOptions & {
|
|
22
|
+
/**
|
|
23
|
+
* When true AND `onlyKillPids` is a non-empty allowlist, those PIDs may be
|
|
24
|
+
* killed after revalidation. Default false — never kill without an allowlist.
|
|
25
|
+
* When {@link killAllOcxOnPort} is also true, any ocx listener on this port
|
|
26
|
+
* may be killed even if it is not in `onlyKillPids`.
|
|
27
|
+
*/
|
|
28
|
+
killOcxHolders?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Explicit PIDs the caller just stopped / hard-killed. An omitted or empty
|
|
31
|
+
* list means no process may be killed — unless {@link killAllOcxOnPort} is set.
|
|
32
|
+
*/
|
|
33
|
+
onlyKillPids?: number[];
|
|
34
|
+
/**
|
|
35
|
+
* When true with `killOcxHolders`, every live ocx listener on this port may be
|
|
36
|
+
* killed (re-checked each scan). Used by post-update restart so a Windows
|
|
37
|
+
* service wrapper that respawns a *new* bun PID mid-reclaim cannot stay
|
|
38
|
+
* protected just because it was absent from the pre-wait allowlist snapshot.
|
|
39
|
+
* Never kills foreign (non-ocx) processes — only allowlisted teardown PIDs
|
|
40
|
+
* and revalidated ocx listeners.
|
|
41
|
+
*/
|
|
42
|
+
killAllOcxOnPort?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* On Windows, force-delete IPv4 TCP rows for this local port via SetTcpEntry.
|
|
45
|
+
* Default true on win32. Never kills foreign processes, never runs while a
|
|
46
|
+
* live foreign / protected ocx listener owns the port, and never runs when
|
|
47
|
+
* the listener scan failed.
|
|
48
|
+
*/
|
|
49
|
+
dropTcpRows?: boolean;
|
|
50
|
+
/** How often to scan for listen PIDs / attempt TCB drop (ms). Default 500. */
|
|
51
|
+
scanIntervalMs?: number;
|
|
52
|
+
listListenPidsFn?: (port: number) => ListenPidScan | number[];
|
|
53
|
+
isAliveFn?: (pid: number) => boolean;
|
|
54
|
+
verifyOcxFn?: (pid: number) => number | null;
|
|
55
|
+
killFn?: (pid: number) => void;
|
|
56
|
+
dropTcpFn?: (port: number) => number | { dropped: number; skippedIpv6: number };
|
|
57
|
+
isAvailableFn?: (port: number, hostname?: string) => Promise<boolean>;
|
|
58
|
+
sleepMs?: (ms: number) => Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse `netstat -ano` (Windows) / `netstat -anlp` listen lines for a port.
|
|
63
|
+
* Exported for unit tests.
|
|
64
|
+
*/
|
|
65
|
+
export function parseListenPidsFromNetstat(output: string, port: number): number[] {
|
|
66
|
+
const pids = new Set<number>();
|
|
67
|
+
const portSuffix = `:${port}`;
|
|
68
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
69
|
+
const line = rawLine.trim();
|
|
70
|
+
if (!/^TCP\b/i.test(line) && !/^tcp\b/i.test(line)) continue;
|
|
71
|
+
const parts = line.split(/\s+/);
|
|
72
|
+
// Prefer the first address token that ends with :port (local), not a later foreign one.
|
|
73
|
+
const localIdx = parts.findIndex(part => part.endsWith(portSuffix) || part.endsWith(`]:${port}`));
|
|
74
|
+
if (localIdx < 0) continue;
|
|
75
|
+
const foreign = parts[localIdx + 1] ?? "";
|
|
76
|
+
// Locale-safe listen detection: English LISTEN*, or unbound foreign wildcard
|
|
77
|
+
// (German ABHÖREN still shows 0.0.0.0:0 / *:*).
|
|
78
|
+
const listenWord = /\bLISTEN/i.test(line);
|
|
79
|
+
const wildcardForeign = /^(0\.0\.0\.0|::|\*|\[::\]):0$/.test(foreign) || foreign === "*:*";
|
|
80
|
+
if (!listenWord && !wildcardForeign) continue;
|
|
81
|
+
const last = parts[parts.length - 1] ?? "";
|
|
82
|
+
const winPid = /^\d+$/.test(last) ? Number(last) : NaN;
|
|
83
|
+
const unixPid = /^(\d+)(?:\/\S*)?$/.exec(last);
|
|
84
|
+
const pid = Number.isSafeInteger(winPid) && winPid > 0
|
|
85
|
+
? winPid
|
|
86
|
+
: unixPid
|
|
87
|
+
? Number(unixPid[1])
|
|
88
|
+
: NaN;
|
|
89
|
+
if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
|
|
90
|
+
}
|
|
91
|
+
return [...pids];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeListenPidScan(result: ListenPidScan | number[]): ListenPidScan {
|
|
95
|
+
if (Array.isArray(result)) return { ok: true, pids: result };
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Prefer English netstat states; fall back to the UI-locale table. */
|
|
100
|
+
function readWindowsNetstatAno(): string {
|
|
101
|
+
const netstat = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\netstat.exe`;
|
|
102
|
+
const cmd = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
|
|
103
|
+
try {
|
|
104
|
+
// chcp 437 forces English LISTENING/ESTABLISHED labels on localized Windows.
|
|
105
|
+
return execFileSync(cmd, ["/d", "/c", `chcp 437>nul & "${netstat}" -ano -p tcp`], {
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
+
timeout: 5000,
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
return execFileSync(netstat, ["-ano", "-p", "tcp"], {
|
|
113
|
+
encoding: "utf-8",
|
|
114
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
115
|
+
timeout: 4000,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Scan for PIDs currently LISTENing on `port`.
|
|
123
|
+
* Distinguishes probe failure (`ok: false`) from a successful empty result.
|
|
124
|
+
*/
|
|
125
|
+
export function scanListenPids(port: number): ListenPidScan {
|
|
126
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
127
|
+
return { ok: false, error: "invalid port" };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
if (process.platform === "win32") {
|
|
131
|
+
return { ok: true, pids: parseListenPidsFromNetstat(readWindowsNetstatAno(), port) };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const output = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
+
timeout: 3000,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
pids: output
|
|
142
|
+
.split(/\r?\n/)
|
|
143
|
+
.map(line => Number(line.trim()))
|
|
144
|
+
.filter(pid => Number.isSafeInteger(pid) && pid > 0),
|
|
145
|
+
};
|
|
146
|
+
} catch (lsofErr) {
|
|
147
|
+
try {
|
|
148
|
+
const output = execFileSync("netstat", ["-anlp"], {
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
151
|
+
timeout: 3000,
|
|
152
|
+
});
|
|
153
|
+
return { ok: true, pids: parseListenPidsFromNetstat(output, Math.trunc(port)) };
|
|
154
|
+
} catch (netstatErr) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: `lsof/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return { ok: false, error: String(error) };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Best-effort PIDs currently LISTENing on `port`. Empty on probe failure. */
|
|
167
|
+
export function listListenPids(port: number): number[] {
|
|
168
|
+
const scan = scanListenPids(port);
|
|
169
|
+
return scan.ok ? scan.pids : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Wait until `port` can bind.
|
|
174
|
+
* Never kills a process unless `killOcxHolders === true` and either
|
|
175
|
+
* `onlyKillPids` is a non-empty allowlist or `killAllOcxOnPort` is set — then
|
|
176
|
+
* revalidates immediately before each kill.
|
|
177
|
+
* Never kills foreign processes. Never drops TCP rows while a live foreign or
|
|
178
|
+
* protected ocx listener owns the port, or when the listener scan failed.
|
|
179
|
+
*/
|
|
180
|
+
export async function reclaimListenPort(
|
|
181
|
+
port: number,
|
|
182
|
+
hostname = "127.0.0.1",
|
|
183
|
+
opts: ReclaimListenPortOptions = {},
|
|
184
|
+
): Promise<boolean> {
|
|
185
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
186
|
+
const intervalMs = opts.intervalMs ?? 100;
|
|
187
|
+
const scanIntervalMs = opts.scanIntervalMs ?? 500;
|
|
188
|
+
const allowedKillPids = new Set(
|
|
189
|
+
(opts.onlyKillPids ?? []).filter(pid => Number.isSafeInteger(pid) && pid > 0),
|
|
190
|
+
);
|
|
191
|
+
const killAllOcx = opts.killAllOcxOnPort === true;
|
|
192
|
+
const mayKill = opts.killOcxHolders === true
|
|
193
|
+
&& (allowedKillPids.size > 0 || killAllOcx);
|
|
194
|
+
const dropTcpRows = opts.dropTcpRows ?? process.platform === "win32";
|
|
195
|
+
const listFn = opts.listListenPidsFn ?? scanListenPids;
|
|
196
|
+
const isAliveFn = opts.isAliveFn ?? isProcessAlive;
|
|
197
|
+
const verifyOcxFn = opts.verifyOcxFn ?? verifyPidIdentity;
|
|
198
|
+
const killFn = opts.killFn ?? killProxy;
|
|
199
|
+
const dropTcpFn = opts.dropTcpFn ?? dropWindowsTcpRowsForLocalPort;
|
|
200
|
+
const isAvailableFn = opts.isAvailableFn ?? isPortAvailable;
|
|
201
|
+
const sleep = opts.sleepMs ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
202
|
+
|
|
203
|
+
const deadline = Date.now() + timeoutMs;
|
|
204
|
+
let lastScan = 0;
|
|
205
|
+
const killed = new Set<number>();
|
|
206
|
+
|
|
207
|
+
for (;;) {
|
|
208
|
+
if (await isAvailableFn(port, hostname)) return true;
|
|
209
|
+
if (Date.now() >= deadline) return false;
|
|
210
|
+
|
|
211
|
+
if (Date.now() - lastScan >= scanIntervalMs) {
|
|
212
|
+
lastScan = Date.now();
|
|
213
|
+
|
|
214
|
+
const scan = normalizeListenPidScan(listFn(port));
|
|
215
|
+
if (!scan.ok) {
|
|
216
|
+
// Failed probe ≠ empty listeners: do not kill and do not reset TCP rows.
|
|
217
|
+
await sleep(intervalMs);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let foreignLive = false;
|
|
222
|
+
let protectedOcxListener = false;
|
|
223
|
+
|
|
224
|
+
for (const pid of scan.pids) {
|
|
225
|
+
if (pid === process.pid) continue;
|
|
226
|
+
if (!isAliveFn(pid)) {
|
|
227
|
+
// Clear "already tried" so a later respawn that reuses this PID slot
|
|
228
|
+
// is not skipped (Windows service :loop / npm rename respawns).
|
|
229
|
+
killed.delete(pid);
|
|
230
|
+
continue; // Windows may still list a dead owner briefly
|
|
231
|
+
}
|
|
232
|
+
const isOcx = verifyOcxFn(pid) === pid;
|
|
233
|
+
const allowlisted = allowedKillPids.has(pid);
|
|
234
|
+
// Pre-update PIDs can fail verify while still LISTENing (dead owner still
|
|
235
|
+
// listed, or cmdline probe raced). Allowlisted teardown PIDs may be killed;
|
|
236
|
+
// unknown foreign claimants must remain fail-closed.
|
|
237
|
+
if (!isOcx) {
|
|
238
|
+
if (mayKill && allowlisted) {
|
|
239
|
+
if (!killed.has(pid)) {
|
|
240
|
+
try {
|
|
241
|
+
killFn(pid);
|
|
242
|
+
killed.add(pid);
|
|
243
|
+
} catch {
|
|
244
|
+
// Kill failed: never SetTcpEntry while the process may still own the port.
|
|
245
|
+
protectedOcxListener = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!isAliveFn(pid)) killed.delete(pid);
|
|
249
|
+
else protectedOcxListener = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
foreignLive = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const mayKillThis = allowlisted || killAllOcx;
|
|
256
|
+
if (!mayKill || !mayKillThis) {
|
|
257
|
+
// Healthy / intentional ocx proxy — never steal its port.
|
|
258
|
+
protectedOcxListener = true;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!killed.has(pid)) {
|
|
262
|
+
// Revalidate immediately before termination.
|
|
263
|
+
if (isAliveFn(pid) && verifyOcxFn(pid) === pid && mayKillThis) {
|
|
264
|
+
try {
|
|
265
|
+
killFn(pid);
|
|
266
|
+
killed.add(pid);
|
|
267
|
+
} catch {
|
|
268
|
+
// Kill failed: keep waiting and never reset this listener's TCP rows.
|
|
269
|
+
protectedOcxListener = true;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// Revalidation failed while the allowlisted listener is still listed live.
|
|
273
|
+
protectedOcxListener = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Respawning supervisors (Windows service :loop) mint a new PID after each
|
|
277
|
+
// kill — clear the per-PID "already tried" bit once the process is gone so a
|
|
278
|
+
// later child with a reused slot is not skipped, and keep reclaiming while live.
|
|
279
|
+
if (!isAliveFn(pid)) {
|
|
280
|
+
killed.delete(pid);
|
|
281
|
+
} else {
|
|
282
|
+
protectedOcxListener = true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (foreignLive || protectedOcxListener) {
|
|
287
|
+
// Foreign app or an unprotected live ocx listener owns the port: never
|
|
288
|
+
// SetTcpEntry-reset their sockets, and fail reclaim once the deadline hits.
|
|
289
|
+
await sleep(intervalMs);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// After hard-kill, browsers often keep ESTABLISHED/CLOSE_WAIT to the dead listener.
|
|
294
|
+
// Reset those IPv4 TCBs (and ghost LISTEN rows) so the configured port can bind again —
|
|
295
|
+
// without killing the browser process. Only safe when no live foreign/protected listener remains.
|
|
296
|
+
if (dropTcpRows) {
|
|
297
|
+
try {
|
|
298
|
+
dropTcpFn(port);
|
|
299
|
+
} catch {
|
|
300
|
+
/* access denied / unsupported — keep waiting */
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await sleep(intervalMs);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when an error means "this port/address is already bound" — the only bind failure
|
|
5
|
+
* that is safe to answer with a retry on another port. Bun/Node surface it as
|
|
6
|
+
* `code: "EADDRINUSE"` or an EADDRINUSE / "address in use" message depending on the API.
|
|
7
|
+
*/
|
|
8
|
+
export function isAddrInUse(err: unknown): boolean {
|
|
9
|
+
if (!err || typeof err !== "object") return false;
|
|
10
|
+
const { code, message } = err as { code?: unknown; message?: unknown };
|
|
11
|
+
if (code === "EADDRINUSE") return true;
|
|
12
|
+
const text = typeof message === "string" ? message.toLowerCase() : "";
|
|
13
|
+
return text.includes("eaddrinuse") || text.includes("in use");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function isPortAvailable(port: number, hostname = "127.0.0.1"): Promise<boolean> {
|
|
17
|
+
return await new Promise(resolve => {
|
|
18
|
+
const server = createServer();
|
|
19
|
+
// Fail closed: EACCES / EADDRNOTAVAIL / EPERM / unknown listen errors mean the
|
|
20
|
+
// requested bind is not available. Only the listening event reports free.
|
|
21
|
+
server.once("error", () => resolve(false));
|
|
22
|
+
server.once("listening", () => {
|
|
23
|
+
server.close(() => resolve(true));
|
|
24
|
+
});
|
|
25
|
+
server.listen({ port, host: hostname });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type WaitForPortOptions = {
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
intervalMs?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Poll until `port` accepts a bind, or until the timeout elapses. */
|
|
35
|
+
export async function waitForPortAvailable(
|
|
36
|
+
port: number,
|
|
37
|
+
hostname = "127.0.0.1",
|
|
38
|
+
opts: WaitForPortOptions = {},
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
41
|
+
const intervalMs = opts.intervalMs ?? 50;
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
for (;;) {
|
|
44
|
+
if (await isPortAvailable(port, hostname)) return true;
|
|
45
|
+
if (Date.now() >= deadline) return false;
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type FindAvailablePortOptions = {
|
|
51
|
+
/** How long to keep retrying the preferred port before falling back to an ephemeral port. */
|
|
52
|
+
preferRetryMs?: number;
|
|
53
|
+
preferRetryIntervalMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* When false, never bind `port: 0` — prefer-retry then throw if the preferred port
|
|
56
|
+
* stays busy. Used for explicit `ocx start --port N` and service-baked pins so an
|
|
57
|
+
* update restart cannot hop to a random ephemeral listener (PR #152 gap).
|
|
58
|
+
*/
|
|
59
|
+
allowEphemeralFallback?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export class PortUnavailableError extends Error {
|
|
63
|
+
readonly port: number;
|
|
64
|
+
constructor(port: number, hostname: string) {
|
|
65
|
+
super(`Port ${port} on ${hostname} is still busy after prefer-retry; refusing ephemeral fallback.`);
|
|
66
|
+
this.name = "PortUnavailableError";
|
|
67
|
+
this.port = port;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function findAvailablePort(
|
|
72
|
+
preferredPort: number,
|
|
73
|
+
hostname = "127.0.0.1",
|
|
74
|
+
opts: FindAvailablePortOptions = {},
|
|
75
|
+
): Promise<number> {
|
|
76
|
+
const preferRetryMs = opts.preferRetryMs ?? 0;
|
|
77
|
+
const allowEphemeral = opts.allowEphemeralFallback !== false;
|
|
78
|
+
// Port 0 asks the OS to select an ephemeral port. Resolve it to that concrete
|
|
79
|
+
// port here so callers never persist or advertise an unusable `:0` endpoint.
|
|
80
|
+
if (preferredPort > 0 && preferRetryMs > 0) {
|
|
81
|
+
if (await waitForPortAvailable(preferredPort, hostname, {
|
|
82
|
+
timeoutMs: preferRetryMs,
|
|
83
|
+
intervalMs: opts.preferRetryIntervalMs ?? 50,
|
|
84
|
+
})) {
|
|
85
|
+
return preferredPort;
|
|
86
|
+
}
|
|
87
|
+
} else if (preferredPort > 0 && (await isPortAvailable(preferredPort, hostname))) {
|
|
88
|
+
return preferredPort;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!allowEphemeral) {
|
|
92
|
+
throw new PortUnavailableError(preferredPort, hostname);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return await new Promise((resolve, reject) => {
|
|
96
|
+
const server = createServer();
|
|
97
|
+
server.once("error", reject);
|
|
98
|
+
server.once("listening", () => {
|
|
99
|
+
const address = server.address();
|
|
100
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
101
|
+
server.close(() => {
|
|
102
|
+
if (port > 0) resolve(port);
|
|
103
|
+
else reject(new Error("failed to allocate an available port"));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
server.listen({ port: 0, host: hostname });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function shouldPersistSelectedPort(
|
|
111
|
+
configPort: number | undefined,
|
|
112
|
+
selectedPort: number,
|
|
113
|
+
preferredPort: number,
|
|
114
|
+
): boolean {
|
|
115
|
+
return selectedPort === preferredPort && configPort !== selectedPort;
|
|
116
|
+
}
|