@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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Microsoft Graph OneDrive helpers (upload / download / ensure folder).
|
|
3
|
+
* Remote root: /OpenCodex/sync/ under the user's OneDrive.
|
|
4
|
+
*/
|
|
5
|
+
import { getValidOneDriveAccessToken } from "./onedrive-auth";
|
|
6
|
+
|
|
7
|
+
const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
8
|
+
const REMOTE_ROOT = "OpenCodex/sync";
|
|
9
|
+
|
|
10
|
+
function encodePath(relativePath: string): string {
|
|
11
|
+
// Graph path encoding: each segment URI-encoded, colon syntax for path
|
|
12
|
+
return relativePath
|
|
13
|
+
.split("/")
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map(seg => encodeURIComponent(seg))
|
|
16
|
+
.join("/");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function authHeaders(): Promise<Headers> {
|
|
20
|
+
const { token } = await getValidOneDriveAccessToken();
|
|
21
|
+
return new Headers({ Authorization: `Bearer ${token}` });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Ensure OpenCodex/sync folder exists (idempotent). */
|
|
25
|
+
export async function ensureSyncFolder(): Promise<void> {
|
|
26
|
+
const headers = await authHeaders();
|
|
27
|
+
// Create OpenCodex then sync via children path
|
|
28
|
+
for (const folder of ["OpenCodex", "OpenCodex/sync"]) {
|
|
29
|
+
const parent = folder.includes("/") ? folder.split("/").slice(0, -1).join("/") : "";
|
|
30
|
+
const name = folder.split("/").pop()!;
|
|
31
|
+
const parentUrl = parent
|
|
32
|
+
? `${GRAPH}/me/drive/root:/${encodePath(parent)}:/children`
|
|
33
|
+
: `${GRAPH}/me/drive/root/children`;
|
|
34
|
+
const res = await fetch(parentUrl, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: headers.get("Authorization")!,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
name,
|
|
42
|
+
folder: {},
|
|
43
|
+
"@microsoft.graph.conflictBehavior": "fail",
|
|
44
|
+
}),
|
|
45
|
+
signal: AbortSignal.timeout(30_000),
|
|
46
|
+
});
|
|
47
|
+
if (res.status === 409 || res.ok) continue;
|
|
48
|
+
// 409 conflict = already exists for some tenants; also accept nameAlreadyExists
|
|
49
|
+
const body = await res.text();
|
|
50
|
+
if (res.status === 409 || body.includes("nameAlreadyExists")) continue;
|
|
51
|
+
// If fail conflict, try again with replace noop by GETting
|
|
52
|
+
const check = await fetch(`${GRAPH}/me/drive/root:/${encodePath(folder)}`, {
|
|
53
|
+
headers,
|
|
54
|
+
signal: AbortSignal.timeout(15_000),
|
|
55
|
+
});
|
|
56
|
+
if (check.ok) continue;
|
|
57
|
+
throw new Error(`failed to ensure OneDrive folder ${folder}: ${res.status} ${body.slice(0, 300)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function uploadTextFile(relativePath: string, content: string | Buffer, contentType = "application/json"): Promise<void> {
|
|
62
|
+
await ensureSyncFolder();
|
|
63
|
+
const headers = await authHeaders();
|
|
64
|
+
const path = `${REMOTE_ROOT}/${relativePath}`.replace(/\/+/g, "/");
|
|
65
|
+
const url = `${GRAPH}/me/drive/root:/${encodePath(path)}:/content`;
|
|
66
|
+
const body = typeof content === "string" ? content : new Uint8Array(content);
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
method: "PUT",
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: headers.get("Authorization")!,
|
|
71
|
+
"Content-Type": contentType,
|
|
72
|
+
},
|
|
73
|
+
body,
|
|
74
|
+
signal: AbortSignal.timeout(120_000),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const text = await res.text();
|
|
78
|
+
throw new Error(`OneDrive upload failed for ${relativePath}: ${res.status} ${text.slice(0, 400)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function downloadFile(relativePath: string): Promise<Buffer | null> {
|
|
83
|
+
const headers = await authHeaders();
|
|
84
|
+
const path = `${REMOTE_ROOT}/${relativePath}`.replace(/\/+/g, "/");
|
|
85
|
+
const url = `${GRAPH}/me/drive/root:/${encodePath(path)}:/content`;
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
headers,
|
|
88
|
+
signal: AbortSignal.timeout(120_000),
|
|
89
|
+
});
|
|
90
|
+
if (res.status === 404) return null;
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
throw new Error(`OneDrive download failed for ${relativePath}: ${res.status} ${text.slice(0, 400)}`);
|
|
94
|
+
}
|
|
95
|
+
return Buffer.from(await res.arrayBuffer());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function remoteFileExists(relativePath: string): Promise<boolean> {
|
|
99
|
+
const headers = await authHeaders();
|
|
100
|
+
const path = `${REMOTE_ROOT}/${relativePath}`.replace(/\/+/g, "/");
|
|
101
|
+
const url = `${GRAPH}/me/drive/root:/${encodePath(path)}`;
|
|
102
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
|
|
103
|
+
return res.ok;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function remoteSyncRootDescription(): string {
|
|
107
|
+
return `OneDrive:/${REMOTE_ROOT}/`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import { getConfigDir } from "../config";
|
|
6
|
+
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
7
|
+
import type { CloudSyncSettings } from "./types";
|
|
8
|
+
|
|
9
|
+
export function cloudSyncSettingsPath(): string {
|
|
10
|
+
return join(getConfigDir(), "cloud-sync.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deviceIdPath(): string {
|
|
14
|
+
return join(getConfigDir(), "device-id");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getOrCreateDeviceId(): string {
|
|
18
|
+
const path = deviceIdPath();
|
|
19
|
+
if (existsSync(path)) {
|
|
20
|
+
try {
|
|
21
|
+
const id = readFileSync(path, "utf8").trim();
|
|
22
|
+
if (id) return id;
|
|
23
|
+
} catch { /* recreate */ }
|
|
24
|
+
}
|
|
25
|
+
const id = createHash("sha256").update(randomBytes(32)).digest("hex").slice(0, 16);
|
|
26
|
+
writeFileSync(path, `${id}\n`, { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
hardenSecretPath(path, { required: true });
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDeviceName(): string {
|
|
32
|
+
try { return hostname() || "unknown-device"; } catch { return "unknown-device"; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readCloudSyncSettings(): CloudSyncSettings {
|
|
36
|
+
const path = cloudSyncSettingsPath();
|
|
37
|
+
if (!existsSync(path)) return { provider: "onedrive" };
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as CloudSyncSettings;
|
|
40
|
+
return { ...raw, provider: "onedrive" };
|
|
41
|
+
} catch {
|
|
42
|
+
return { provider: "onedrive" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeCloudSyncSettings(settings: CloudSyncSettings): void {
|
|
47
|
+
const path = cloudSyncSettingsPath();
|
|
48
|
+
const next: CloudSyncSettings = { ...settings, provider: "onedrive" };
|
|
49
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
50
|
+
hardenSecretPath(path, { required: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolve Microsoft public client id: flag > env > settings. */
|
|
54
|
+
export function resolveClientId(explicit?: string): string {
|
|
55
|
+
const fromFlag = explicit?.trim();
|
|
56
|
+
if (fromFlag) return fromFlag;
|
|
57
|
+
const fromEnv = process.env.OPENCODEX_MS_CLIENT_ID?.trim() || process.env.OPENCODEX_ONEDRIVE_CLIENT_ID?.trim();
|
|
58
|
+
if (fromEnv) return fromEnv;
|
|
59
|
+
const fromSettings = readCloudSyncSettings().clientId?.trim();
|
|
60
|
+
if (fromSettings) return fromSettings;
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Microsoft client_id required.\n"
|
|
63
|
+
+ " 1) Register an app at https://portal.azure.com → App registrations\n"
|
|
64
|
+
+ " 2) Supported accounts: personal + organizational\n"
|
|
65
|
+
+ " 3) Platform: Mobile and desktop (or none). Device-code login does not need a redirect URI.\n"
|
|
66
|
+
+ " Do NOT use http://localhost:10100/#cloud — Azure rejects # fragments in redirect URIs.\n"
|
|
67
|
+
+ " 4) Authentication → Allow public client flows = Yes\n"
|
|
68
|
+
+ " (or Manifest: \"allowPublicClient\": true). Device code requires a public/mobile client.\n"
|
|
69
|
+
+ " 5) API permissions (delegated Microsoft Graph): User.Read, Files.ReadWrite, offline_access\n"
|
|
70
|
+
+ " Personal-MSA-only apps use /consumers (set automatically).\n"
|
|
71
|
+
+ " 6) Then:\n"
|
|
72
|
+
+ " ocx sync-cloud set-client-id <YOUR_CLIENT_ID>\n"
|
|
73
|
+
+ " or export OPENCODEX_MS_CLIENT_ID=<YOUR_CLIENT_ID>",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push / pull OpenCodex local state to OneDrive.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { getConfigDir, loadConfig, saveConfig } from "../config";
|
|
7
|
+
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
8
|
+
import { downloadFile, remoteSyncRootDescription, uploadTextFile } from "./onedrive-graph";
|
|
9
|
+
import {
|
|
10
|
+
getDeviceName,
|
|
11
|
+
getOrCreateDeviceId,
|
|
12
|
+
readCloudSyncSettings,
|
|
13
|
+
writeCloudSyncSettings,
|
|
14
|
+
} from "./settings";
|
|
15
|
+
import type { CloudSyncManifest } from "./types";
|
|
16
|
+
import { decryptVault, encryptVault, packVaultFiles, unpackVaultFiles } from "./vault";
|
|
17
|
+
|
|
18
|
+
const VAULT_FILES = ["auth.json", "codex-accounts.json"] as const;
|
|
19
|
+
const CONFIG_FILE = "config.json";
|
|
20
|
+
const MANIFEST_FILE = "manifest.json";
|
|
21
|
+
const VAULT_FILE = "vault.enc";
|
|
22
|
+
const USAGE_FILE = "usage.jsonl";
|
|
23
|
+
|
|
24
|
+
function localPath(name: string): string {
|
|
25
|
+
return join(getConfigDir(), name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readLocalOptional(name: string): string | null {
|
|
29
|
+
const path = localPath(name);
|
|
30
|
+
if (!existsSync(path)) return null;
|
|
31
|
+
return readFileSync(path, "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeLocal(name: string, content: string, mode = 0o600): void {
|
|
35
|
+
const path = localPath(name);
|
|
36
|
+
writeFileSync(path, content, { encoding: "utf8", mode });
|
|
37
|
+
hardenSecretPath(path, { required: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PushOptions {
|
|
41
|
+
passphrase?: string;
|
|
42
|
+
includeUsage?: boolean;
|
|
43
|
+
includeVault?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PullOptions {
|
|
47
|
+
passphrase?: string;
|
|
48
|
+
/** Overwrite local config/auth without interactive confirm (CLI --yes). */
|
|
49
|
+
yes?: boolean;
|
|
50
|
+
includeUsage?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SyncResult {
|
|
54
|
+
direction: "push" | "pull";
|
|
55
|
+
remoteRoot: string;
|
|
56
|
+
files: string[];
|
|
57
|
+
hasVault: boolean;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function pushToOneDrive(options: PushOptions = {}): Promise<SyncResult> {
|
|
62
|
+
const settings = readCloudSyncSettings();
|
|
63
|
+
const includeUsage = options.includeUsage ?? settings.includeUsage === true;
|
|
64
|
+
const includeVault = options.includeVault ?? Boolean(options.passphrase);
|
|
65
|
+
if (includeVault && !options.passphrase) {
|
|
66
|
+
throw new Error("vault sync requires --passphrase (min 8 chars)");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const files: string[] = [];
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const configJson = `${JSON.stringify(config, null, 2)}\n`;
|
|
72
|
+
await uploadTextFile(CONFIG_FILE, configJson);
|
|
73
|
+
files.push(CONFIG_FILE);
|
|
74
|
+
|
|
75
|
+
let hasVault = false;
|
|
76
|
+
if (includeVault && options.passphrase) {
|
|
77
|
+
const vaultFiles: Record<string, string> = {};
|
|
78
|
+
for (const name of VAULT_FILES) {
|
|
79
|
+
const content = readLocalOptional(name);
|
|
80
|
+
if (content != null) vaultFiles[name] = content;
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(vaultFiles).length === 0) {
|
|
83
|
+
throw new Error("no vault files found (auth.json / codex-accounts.json)");
|
|
84
|
+
}
|
|
85
|
+
const packed = packVaultFiles(vaultFiles);
|
|
86
|
+
const blob = encryptVault(packed, options.passphrase);
|
|
87
|
+
await uploadTextFile(VAULT_FILE, blob, "application/octet-stream");
|
|
88
|
+
files.push(VAULT_FILE);
|
|
89
|
+
hasVault = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (includeUsage) {
|
|
93
|
+
const usage = readLocalOptional(USAGE_FILE);
|
|
94
|
+
if (usage != null) {
|
|
95
|
+
await uploadTextFile(USAGE_FILE, usage, "application/x-ndjson");
|
|
96
|
+
files.push(USAGE_FILE);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const updatedAt = new Date().toISOString();
|
|
101
|
+
const manifest: CloudSyncManifest = {
|
|
102
|
+
version: 1,
|
|
103
|
+
updatedAt,
|
|
104
|
+
deviceId: getOrCreateDeviceId(),
|
|
105
|
+
deviceName: getDeviceName(),
|
|
106
|
+
files,
|
|
107
|
+
hasVault,
|
|
108
|
+
includeUsage,
|
|
109
|
+
};
|
|
110
|
+
await uploadTextFile(MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
111
|
+
files.push(MANIFEST_FILE);
|
|
112
|
+
|
|
113
|
+
writeCloudSyncSettings({
|
|
114
|
+
...settings,
|
|
115
|
+
lastSyncAt: updatedAt,
|
|
116
|
+
lastSyncDirection: "push",
|
|
117
|
+
lastDeviceId: manifest.deviceId,
|
|
118
|
+
includeUsage,
|
|
119
|
+
includeVault,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
direction: "push",
|
|
124
|
+
remoteRoot: remoteSyncRootDescription(),
|
|
125
|
+
files,
|
|
126
|
+
hasVault,
|
|
127
|
+
updatedAt,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function pullFromOneDrive(options: PullOptions = {}): Promise<SyncResult> {
|
|
132
|
+
if (!options.yes) {
|
|
133
|
+
throw new Error("pull overwrites local state — re-run with --yes to confirm");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const manifestBuf = await downloadFile(MANIFEST_FILE);
|
|
137
|
+
if (!manifestBuf) {
|
|
138
|
+
throw new Error(`no cloud sync found at ${remoteSyncRootDescription()} — push from another machine first`);
|
|
139
|
+
}
|
|
140
|
+
const manifest = JSON.parse(manifestBuf.toString("utf8")) as CloudSyncManifest;
|
|
141
|
+
if (manifest.version !== 1) throw new Error(`unsupported remote manifest version ${manifest.version}`);
|
|
142
|
+
|
|
143
|
+
const files: string[] = [MANIFEST_FILE];
|
|
144
|
+
const configBuf = await downloadFile(CONFIG_FILE);
|
|
145
|
+
if (configBuf) {
|
|
146
|
+
// Validate by parsing + saveConfig path (structural checks)
|
|
147
|
+
const raw = JSON.parse(configBuf.toString("utf8"));
|
|
148
|
+
saveConfig(raw);
|
|
149
|
+
files.push(CONFIG_FILE);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let hasVault = false;
|
|
153
|
+
if (manifest.hasVault || manifest.files.includes(VAULT_FILE)) {
|
|
154
|
+
if (!options.passphrase) {
|
|
155
|
+
throw new Error("remote has encrypted vault — provide --passphrase to restore accounts");
|
|
156
|
+
}
|
|
157
|
+
const vaultBuf = await downloadFile(VAULT_FILE);
|
|
158
|
+
if (vaultBuf) {
|
|
159
|
+
const plain = decryptVault(vaultBuf, options.passphrase);
|
|
160
|
+
const payload = unpackVaultFiles(plain);
|
|
161
|
+
for (const [name, content] of Object.entries(payload.files)) {
|
|
162
|
+
if (!VAULT_FILES.includes(name as typeof VAULT_FILES[number])) continue;
|
|
163
|
+
writeLocal(name, content.endsWith("\n") ? content : `${content}\n`);
|
|
164
|
+
files.push(name);
|
|
165
|
+
}
|
|
166
|
+
hasVault = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const wantUsage = options.includeUsage ?? manifest.includeUsage;
|
|
171
|
+
if (wantUsage) {
|
|
172
|
+
const usageBuf = await downloadFile(USAGE_FILE);
|
|
173
|
+
if (usageBuf) {
|
|
174
|
+
// Merge: append remote lines not already present (simple content-hash set of last 50k lines)
|
|
175
|
+
const local = readLocalOptional(USAGE_FILE) ?? "";
|
|
176
|
+
const localLines = new Set(local.split("\n").filter(Boolean));
|
|
177
|
+
const remoteLines = usageBuf.toString("utf8").split("\n").filter(Boolean);
|
|
178
|
+
const merged = [...localLines];
|
|
179
|
+
for (const line of remoteLines) {
|
|
180
|
+
if (!localLines.has(line)) merged.push(line);
|
|
181
|
+
}
|
|
182
|
+
writeLocal(USAGE_FILE, `${merged.join("\n")}\n`);
|
|
183
|
+
files.push(USAGE_FILE);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const settings = readCloudSyncSettings();
|
|
188
|
+
writeCloudSyncSettings({
|
|
189
|
+
...settings,
|
|
190
|
+
lastSyncAt: new Date().toISOString(),
|
|
191
|
+
lastSyncDirection: "pull",
|
|
192
|
+
lastDeviceId: getOrCreateDeviceId(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
direction: "pull",
|
|
197
|
+
remoteRoot: remoteSyncRootDescription(),
|
|
198
|
+
files,
|
|
199
|
+
hasVault,
|
|
200
|
+
updatedAt: manifest.updatedAt,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function readRemoteManifest(): Promise<CloudSyncManifest | null> {
|
|
205
|
+
const buf = await downloadFile(MANIFEST_FILE);
|
|
206
|
+
if (!buf) return null;
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(buf.toString("utf8")) as CloudSyncManifest;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Local cloud-sync settings (stored in ~/.opencodex/cloud-sync.json). */
|
|
2
|
+
|
|
3
|
+
/** Microsoft identity platform tenant path segment for OAuth. */
|
|
4
|
+
export type MsAuthority = "common" | "consumers" | "organizations";
|
|
5
|
+
|
|
6
|
+
export interface CloudSyncSettings {
|
|
7
|
+
provider: "onedrive";
|
|
8
|
+
/** Azure AD / Entra application (public client) ID. */
|
|
9
|
+
clientId?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional client secret — only if the Azure app is a confidential (Web) client.
|
|
12
|
+
* Prefer public client: Authentication → Allow public client flows = Yes (no secret).
|
|
13
|
+
*/
|
|
14
|
+
clientSecret?: string;
|
|
15
|
+
/**
|
|
16
|
+
* login.microsoftonline.com/{msAuthority}/…
|
|
17
|
+
* Personal-MSA-only apps must use "consumers" (not "common").
|
|
18
|
+
*/
|
|
19
|
+
msAuthority?: MsAuthority;
|
|
20
|
+
/**
|
|
21
|
+
* Fixed loopback port for browser OAuth (default 18765).
|
|
22
|
+
* Azure redirect URI must be exactly: http://localhost:{port}
|
|
23
|
+
*/
|
|
24
|
+
loopbackPort?: number;
|
|
25
|
+
/** Last successful push/pull ISO timestamp. */
|
|
26
|
+
lastSyncAt?: string;
|
|
27
|
+
lastSyncDirection?: "push" | "pull";
|
|
28
|
+
lastDeviceId?: string;
|
|
29
|
+
/** Include usage.jsonl on push/pull. Default false (can grow large). */
|
|
30
|
+
includeUsage?: boolean;
|
|
31
|
+
/** Include encrypted vault (auth + codex-accounts). Requires passphrase. Default true when pushing with --passphrase. */
|
|
32
|
+
includeVault?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OneDriveTokenStore {
|
|
36
|
+
accessToken: string;
|
|
37
|
+
refreshToken: string;
|
|
38
|
+
expiresAt: number;
|
|
39
|
+
scope?: string;
|
|
40
|
+
account?: string;
|
|
41
|
+
clientId: string;
|
|
42
|
+
/** Authority used when this token was issued (must match refresh). */
|
|
43
|
+
authority?: MsAuthority;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CloudSyncManifest {
|
|
47
|
+
version: 1;
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
deviceId: string;
|
|
50
|
+
deviceName: string;
|
|
51
|
+
/** Relative paths present in the remote folder. */
|
|
52
|
+
files: string[];
|
|
53
|
+
/** true if vault.enc is present and encrypted. */
|
|
54
|
+
hasVault: boolean;
|
|
55
|
+
includeUsage: boolean;
|
|
56
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passphrase-encrypted vault for sensitive OpenCodex state (auth / account pools).
|
|
3
|
+
* Format: base64(JSON header) + "." + base64(iv||ciphertext||tag) is avoided —
|
|
4
|
+
* single binary envelope: magic | version | salt | iv | tag | ciphertext
|
|
5
|
+
*/
|
|
6
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const MAGIC = Buffer.from("OCXV1", "utf8");
|
|
9
|
+
const VERSION = 1;
|
|
10
|
+
const SALT_LEN = 16;
|
|
11
|
+
const IV_LEN = 12;
|
|
12
|
+
const TAG_LEN = 16;
|
|
13
|
+
const KEY_LEN = 32;
|
|
14
|
+
const SCRYPT_N = 16384;
|
|
15
|
+
const SCRYPT_R = 8;
|
|
16
|
+
const SCRYPT_P = 1;
|
|
17
|
+
|
|
18
|
+
function deriveKey(passphrase: string, salt: Buffer): Buffer {
|
|
19
|
+
return scryptSync(passphrase, salt, KEY_LEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 64 * 1024 * 1024 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function encryptVault(plaintext: string | Buffer, passphrase: string): Buffer {
|
|
23
|
+
if (!passphrase || passphrase.length < 8) {
|
|
24
|
+
throw new Error("vault passphrase must be at least 8 characters");
|
|
25
|
+
}
|
|
26
|
+
const salt = randomBytes(SALT_LEN);
|
|
27
|
+
const iv = randomBytes(IV_LEN);
|
|
28
|
+
const key = deriveKey(passphrase, salt);
|
|
29
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
30
|
+
const data = typeof plaintext === "string" ? Buffer.from(plaintext, "utf8") : plaintext;
|
|
31
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
32
|
+
const tag = cipher.getAuthTag();
|
|
33
|
+
return Buffer.concat([
|
|
34
|
+
MAGIC,
|
|
35
|
+
Buffer.from([VERSION]),
|
|
36
|
+
salt,
|
|
37
|
+
iv,
|
|
38
|
+
tag,
|
|
39
|
+
encrypted,
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function decryptVault(blob: Buffer, passphrase: string): Buffer {
|
|
44
|
+
if (blob.length < MAGIC.length + 1 + SALT_LEN + IV_LEN + TAG_LEN + 1) {
|
|
45
|
+
throw new Error("vault blob is too short or corrupt");
|
|
46
|
+
}
|
|
47
|
+
if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
48
|
+
throw new Error("vault blob has invalid magic (not an OpenCodex vault)");
|
|
49
|
+
}
|
|
50
|
+
const version = blob[MAGIC.length];
|
|
51
|
+
if (version !== VERSION) throw new Error(`unsupported vault version ${version}`);
|
|
52
|
+
let offset = MAGIC.length + 1;
|
|
53
|
+
const salt = blob.subarray(offset, offset + SALT_LEN); offset += SALT_LEN;
|
|
54
|
+
const iv = blob.subarray(offset, offset + IV_LEN); offset += IV_LEN;
|
|
55
|
+
const tag = blob.subarray(offset, offset + TAG_LEN); offset += TAG_LEN;
|
|
56
|
+
const encrypted = blob.subarray(offset);
|
|
57
|
+
const key = deriveKey(passphrase, salt);
|
|
58
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
59
|
+
decipher.setAuthTag(tag);
|
|
60
|
+
try {
|
|
61
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error("vault decrypt failed — wrong passphrase or corrupt file");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface VaultPayload {
|
|
68
|
+
version: 1;
|
|
69
|
+
createdAt: string;
|
|
70
|
+
/** Filename → utf8 content */
|
|
71
|
+
files: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function packVaultFiles(files: Record<string, string>): string {
|
|
75
|
+
const payload: VaultPayload = {
|
|
76
|
+
version: 1,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
files,
|
|
79
|
+
};
|
|
80
|
+
return JSON.stringify(payload);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function unpackVaultFiles(plaintext: Buffer): VaultPayload {
|
|
84
|
+
const parsed = JSON.parse(plaintext.toString("utf8")) as VaultPayload;
|
|
85
|
+
if (parsed?.version !== 1 || !parsed.files || typeof parsed.files !== "object") {
|
|
86
|
+
throw new Error("vault payload schema invalid");
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Canonical persisted Codex pool-account id format. */
|
|
2
|
+
export const CODEX_ACCOUNT_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
3
|
+
|
|
4
|
+
// Account credentials are persisted in JSON object maps. These names have
|
|
5
|
+
// special meaning on ordinary JavaScript objects and must never be admitted as
|
|
6
|
+
// user-controlled keys.
|
|
7
|
+
const RESERVED_CODEX_ACCOUNT_IDS = new Set(["__proto__", "prototype", "constructor"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stable internal id under which the Codex Desktop login participates in account rotation.
|
|
11
|
+
* It is reserved and must never be used as a public namespace target or pool-account id.
|
|
12
|
+
*/
|
|
13
|
+
export const MAIN_CODEX_ACCOUNT_ID = "__main__";
|
|
14
|
+
|
|
15
|
+
export function isValidCodexAccountId(accountId: unknown): accountId is string {
|
|
16
|
+
return typeof accountId === "string"
|
|
17
|
+
&& accountId !== MAIN_CODEX_ACCOUNT_ID
|
|
18
|
+
&& CODEX_ACCOUNT_ID_RE.test(accountId)
|
|
19
|
+
&& !RESERVED_CODEX_ACCOUNT_IDS.has(accountId.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type CodexAccountIdentityRow = { id: string; isMain: boolean };
|
|
23
|
+
|
|
24
|
+
/** Legacy invalid rows remain loadable for cleanup, but never participate in routing. */
|
|
25
|
+
export function isSelectableCodexPoolAccount(account: CodexAccountIdentityRow): boolean {
|
|
26
|
+
return !account.isMain && isValidCodexAccountId(account.id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A pre-validation pool row that collides with the internal Desktop-account sentinel. */
|
|
30
|
+
export function hasLegacyMainCodexPoolAccount(
|
|
31
|
+
accounts: readonly CodexAccountIdentityRow[] | undefined,
|
|
32
|
+
): boolean {
|
|
33
|
+
return accounts?.some(account => !account.isMain && account.id === MAIN_CODEX_ACCOUNT_ID) ?? false;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { CodexAccount } from "../types";
|
|
3
|
+
|
|
4
|
+
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
5
|
+
|
|
6
|
+
export function createCodexAccountLogLabel(existingLabels: Iterable<string | undefined | null> = []): string {
|
|
7
|
+
const used = new Set([...existingLabels].filter((value): value is string => !!value));
|
|
8
|
+
for (let i = 0; i < 16; i++) {
|
|
9
|
+
const label = `p${randomBytes(3).toString("hex")}`;
|
|
10
|
+
if (!used.has(label)) return label;
|
|
11
|
+
}
|
|
12
|
+
return `p${randomBytes(6).toString("hex").slice(0, 6)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fallbackCodexAccountLogLabel(accountId: string): string {
|
|
16
|
+
return `p${createHash("sha256").update(accountId).digest("hex").slice(0, 6)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function codexAccountLogLabel(account: CodexAccount): string {
|
|
20
|
+
return CODEX_ACCOUNT_LOG_LABEL_RE.test(account.logLabel ?? "")
|
|
21
|
+
? account.logLabel!
|
|
22
|
+
: fallbackCodexAccountLogLabel(account.id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function withCodexAccountLogLabel(
|
|
26
|
+
account: Omit<CodexAccount, "logLabel"> & Partial<Pick<CodexAccount, "logLabel">>,
|
|
27
|
+
existingAccounts: readonly CodexAccount[],
|
|
28
|
+
): CodexAccount {
|
|
29
|
+
if (account.logLabel && CODEX_ACCOUNT_LOG_LABEL_RE.test(account.logLabel)) return account as CodexAccount;
|
|
30
|
+
return {
|
|
31
|
+
...account,
|
|
32
|
+
logLabel: createCodexAccountLogLabel(existingAccounts.map(existing => existing.logLabel)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { removeCodexAccountCredential } from "./account-store";
|
|
2
|
+
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
3
|
+
import { getMainChatgptAccountId } from "./auth-collision";
|
|
4
|
+
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
5
|
+
import { clearAccountQuota } from "./quota";
|
|
6
|
+
import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
|
|
7
|
+
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
8
|
+
import { clearMainAccountInfoCache } from "./main-account-cache";
|
|
9
|
+
import { forgetCodexAccountPause } from "./account-pause";
|
|
10
|
+
import type { OcxConfig } from "../types";
|
|
11
|
+
|
|
12
|
+
let observedMainChatgptAccountId: string | undefined;
|
|
13
|
+
|
|
14
|
+
export function purgeCodexAccountRuntimeState(accountId: string): void {
|
|
15
|
+
clearAccountNeedsReauth(accountId);
|
|
16
|
+
clearAccountQuota(accountId);
|
|
17
|
+
clearThreadAccountMapForAccount(accountId);
|
|
18
|
+
clearCodexUpstreamHealthForAccount(accountId);
|
|
19
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) clearMainAccountInfoCache();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The main Codex login is stored under the stable `__main__` alias, while
|
|
24
|
+
* `~/.codex/auth.json` can be replaced with credentials for another physical
|
|
25
|
+
* ChatGPT account. Drop alias-keyed runtime state when that identity changes so
|
|
26
|
+
* cooldown, quota, reauth, and thread affinity do not leak across accounts.
|
|
27
|
+
*/
|
|
28
|
+
export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
29
|
+
const currentAccountId = getMainChatgptAccountId();
|
|
30
|
+
// A missing/malformed auth.json is an unknown identity, not a confirmed account switch. Keep the
|
|
31
|
+
// prior observation and its safety state until a real account id can be read again.
|
|
32
|
+
if (currentAccountId === null) return false;
|
|
33
|
+
const previousAccountId = observedMainChatgptAccountId;
|
|
34
|
+
observedMainChatgptAccountId = currentAccountId;
|
|
35
|
+
if (previousAccountId === undefined || previousAccountId === currentAccountId) return false;
|
|
36
|
+
|
|
37
|
+
purgeCodexAccountRuntimeState(MAIN_CODEX_ACCOUNT_ID);
|
|
38
|
+
setMainAccountPlan(null);
|
|
39
|
+
invalidateCodexWebSocketsForAccount(MAIN_CODEX_ACCOUNT_ID);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetMainCodexAccountIdentityTrackingForTests(): void {
|
|
44
|
+
observedMainChatgptAccountId = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string): void {
|
|
48
|
+
removeCodexAccountCredential(accountId);
|
|
49
|
+
runtimeConfig.codexAccounts = (runtimeConfig.codexAccounts ?? [])
|
|
50
|
+
.filter(account => account.isMain || account.id !== accountId);
|
|
51
|
+
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
52
|
+
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
53
|
+
purgeCodexAccountRuntimeState(accountId);
|
|
54
|
+
invalidateCodexWebSocketsForAccount(accountId);
|
|
55
|
+
}
|