@bitkyc08/opencodex 2.80.0 → 2.82.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/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Command Code config export.
|
|
2
|
+
import type { ExportContext, ManagedContribution } from "./contracts";
|
|
3
|
+
import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata";
|
|
4
|
+
import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
|
|
5
|
+
import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
6
|
+
|
|
7
|
+
export interface CommandCodeModelEntry {
|
|
8
|
+
reasoningEfforts?: string[];
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandCodeProviderBlock {
|
|
13
|
+
name: "OpenCodex";
|
|
14
|
+
api: "openai-completions";
|
|
15
|
+
baseURL: string;
|
|
16
|
+
apiKey: string | boolean;
|
|
17
|
+
models: Record<string, CommandCodeModelEntry>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CommandCodeGeneratedConfig {
|
|
21
|
+
provider: Record<string, CommandCodeProviderBlock>;
|
|
22
|
+
/** The root a pre-existing target already uses; see {@link commandCodeProviderRoot}. */
|
|
23
|
+
providers?: Record<string, CommandCodeProviderBlock>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* LOSSY spelling-collision key for a routed selector — not model identity.
|
|
28
|
+
*
|
|
29
|
+
* One provider model reaches this exporter under interchangeable spellings: the raw
|
|
30
|
+
* selector keeps inner slashes (`command-code/deepseek/deepseek-v4.1-flash`, what
|
|
31
|
+
* `/v1/models` publishes) while the Codex-facing form encodes them as dashes
|
|
32
|
+
* (`command-code/deepseek-deepseek-v4.1-flash`, what `~/.codex/config.toml` stores and
|
|
33
|
+
* what an operator typing `--model` copies). Command Code addresses models by exact key,
|
|
34
|
+
* so a catalog that carries both spellings can resolve the active model against one and
|
|
35
|
+
* miss it in the other.
|
|
36
|
+
*
|
|
37
|
+
* The slash is meaningful: it separates the provider from the model id, and a provider id
|
|
38
|
+
* never contains one. But the encoding is NOT bijective — `p/a/b` and `p/a-b` are
|
|
39
|
+
* different model ids that encode to the same string. That is the same lossy relation
|
|
40
|
+
* `slugEquivalenceKey` documents in src/providers/slug-codec.ts and the same ambiguity
|
|
41
|
+
* `resolveSlugAliasCollisions` guards in the catalog. Callers must therefore never drop
|
|
42
|
+
* a row on this key alone; buildCommandCodeClientConfig additionally compares provider
|
|
43
|
+
* and id before folding two rows into one.
|
|
44
|
+
*/
|
|
45
|
+
function canonicalSpellingOf(namespaced: string): string {
|
|
46
|
+
const slash = namespaced.indexOf("/");
|
|
47
|
+
if (slash <= 0) return namespaced;
|
|
48
|
+
return namespaced.slice(0, slash) + "/" + namespaced.slice(slash + 1).replaceAll("/", "-");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildCommandCodeClientConfig(ctx: ExportContext): CommandCodeGeneratedConfig {
|
|
52
|
+
const models: Record<string, CommandCodeModelEntry> = {};
|
|
53
|
+
// Fold interchangeable spellings of one model onto a single key. The first
|
|
54
|
+
// occurrence wins per provider+id identity; distinct model ids that happen to
|
|
55
|
+
// share a lossy spelling collision must both remain addressable.
|
|
56
|
+
const emittedIdentitiesByCollision = new Map<string, Set<string>>();
|
|
57
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
58
|
+
const emittedKey = model.namespaced;
|
|
59
|
+
const collisionKey = canonicalSpellingOf(model.namespaced);
|
|
60
|
+
const identity = `${model.provider}\u0000${model.id}`;
|
|
61
|
+
const identities = emittedIdentitiesByCollision.get(collisionKey) ?? new Set<string>();
|
|
62
|
+
if (identities.has(identity)) continue;
|
|
63
|
+
identities.add(identity);
|
|
64
|
+
emittedIdentitiesByCollision.set(collisionKey, identities);
|
|
65
|
+
const entry: CommandCodeModelEntry = {};
|
|
66
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
67
|
+
if (context !== undefined) {
|
|
68
|
+
entry.contextWindow = context;
|
|
69
|
+
}
|
|
70
|
+
const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
|
|
71
|
+
?.filter(effort => ["low", "medium", "high", "xhigh", "max"].includes(effort));
|
|
72
|
+
if (efforts && efforts.length > 0) {
|
|
73
|
+
entry.reasoningEfforts = efforts;
|
|
74
|
+
}
|
|
75
|
+
models[emittedKey] = entry;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
provider: {
|
|
79
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
80
|
+
name: "OpenCodex",
|
|
81
|
+
api: "openai-completions",
|
|
82
|
+
baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1",
|
|
83
|
+
apiKey: false,
|
|
84
|
+
models,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The root key Command Code actually reads: `provider`, else `providers`.
|
|
92
|
+
*
|
|
93
|
+
* Verified against the published client (`command-code@1.66.0`, `dist/cli.mjs`):
|
|
94
|
+
* `const o = e.provider ?? e.providers;` — singular wins, and the plural branch is
|
|
95
|
+
* only reached when the singular root is nullish (`null` or `undefined`). Adding a
|
|
96
|
+
* singular root to a document that already carries a plural one therefore does not
|
|
97
|
+
* merge the two: the old entries stay on disk, byte for byte, and become invisible
|
|
98
|
+
* to the consumer.
|
|
99
|
+
*
|
|
100
|
+
* Evaluates nullish precedence before container validation so that non-nullish
|
|
101
|
+
* invalid singular values (such as strings or arrays) remain selected and are refused
|
|
102
|
+
* by the container guard rather than falling through to write an unreachable plural block.
|
|
103
|
+
*/
|
|
104
|
+
export function commandCodeProviderRoot(document: unknown): "provider" | "providers" {
|
|
105
|
+
const doc = document as { provider?: unknown; providers?: unknown } | null | undefined;
|
|
106
|
+
if (doc?.provider !== undefined && doc?.provider !== null) return "provider";
|
|
107
|
+
if (doc?.providers !== undefined && doc?.providers !== null) return "providers";
|
|
108
|
+
// Nothing established yet: the singular root is the one this exporter writes.
|
|
109
|
+
return "provider";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function summarizeCommandCode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
113
|
+
const doc = document as CommandCodeGeneratedConfig | undefined;
|
|
114
|
+
const root = commandCodeProviderRoot(document);
|
|
115
|
+
const models = Object.values(doc?.[root]?.[OPENCODE_PROVIDER_ID]?.models ?? {}) as CommandCodeModelEntry[];
|
|
116
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The provider block, written under whichever root the target document already uses.
|
|
121
|
+
*
|
|
122
|
+
* `ExportContext` carries the parsed target document so the root can be chosen from
|
|
123
|
+
* what is on disk rather than assumed. A fresh file gets the singular root this
|
|
124
|
+
* exporter has always written; a document that already carries a plural root keeps
|
|
125
|
+
* it, so enabling OpenCodex never makes the user's other providers unreadable.
|
|
126
|
+
*/
|
|
127
|
+
export function buildCommandCodeContribution(ctx: ExportContext): ManagedContribution {
|
|
128
|
+
const doc = buildCommandCodeClientConfig(ctx);
|
|
129
|
+
const root = commandCodeProviderRoot(ctx.document);
|
|
130
|
+
return singleFragment("commandcode", [root, OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
|
|
131
|
+
}
|
|
@@ -185,6 +185,17 @@ export interface ExportContext {
|
|
|
185
185
|
*/
|
|
186
186
|
config?: OcxConfig;
|
|
187
187
|
droidReasoningDefaults?: DroidReasoningDefaults;
|
|
188
|
+
/**
|
|
189
|
+
* The parsed target document, when the caller has one in hand.
|
|
190
|
+
*
|
|
191
|
+
* A client whose reader picks between two roots needs the bytes on disk to
|
|
192
|
+
* choose: Command Code resolves `document.provider ?? document.providers`, so
|
|
193
|
+
* writing the singular root into a document that already carries the plural one
|
|
194
|
+
* leaves the user's providers unreadable rather than merging with them. Absent
|
|
195
|
+
* for `ocx export`, which has no target file — the builder then writes its
|
|
196
|
+
* default root, exactly as before.
|
|
197
|
+
*/
|
|
198
|
+
document?: unknown;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
/** Namespaced model selector to one of that model's declared reasoning efforts. */
|
|
@@ -206,6 +217,7 @@ export type ExportClientId =
|
|
|
206
217
|
| "raycast"
|
|
207
218
|
| "omo"
|
|
208
219
|
| "cline"
|
|
220
|
+
| "commandcode"
|
|
209
221
|
| "kilo"
|
|
210
222
|
| "droid";
|
|
211
223
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
24
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
24
|
+
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
27
|
import { canonicalizeReasoningEfforts } from "../reasoning-effort";
|
|
@@ -69,6 +69,18 @@ import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfig
|
|
|
69
69
|
export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo";
|
|
70
70
|
export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo";
|
|
71
71
|
import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
|
|
72
|
+
import {
|
|
73
|
+
buildCommandCodeClientConfig,
|
|
74
|
+
summarizeCommandCode,
|
|
75
|
+
buildCommandCodeContribution,
|
|
76
|
+
commandCodeProviderRoot,
|
|
77
|
+
type CommandCodeGeneratedConfig,
|
|
78
|
+
type CommandCodeModelEntry,
|
|
79
|
+
type CommandCodeProviderBlock,
|
|
80
|
+
} from "./config-export/commandcode";
|
|
81
|
+
|
|
82
|
+
export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
|
|
83
|
+
export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
|
|
72
84
|
|
|
73
85
|
|
|
74
86
|
|
|
@@ -474,6 +486,43 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
474
486
|
return join(zcodeHomeDir(env, home), "v2", "config.json");
|
|
475
487
|
}
|
|
476
488
|
|
|
489
|
+
/**
|
|
490
|
+
* The home directory Command Code itself resolves: `env.HOME ?? env.USERPROFILE`
|
|
491
|
+
* (`homeDir15` in the published `command-code@1.66.0` bundle).
|
|
492
|
+
*
|
|
493
|
+
* On POSIX `os.homedir()` already reads `$HOME`, so the two agree and `home` is
|
|
494
|
+
* returned as is. On Windows `os.homedir()` reads `%USERPROFILE%` and ignores
|
|
495
|
+
* `HOME`, which Git for Windows, MSYS2 and Cygwin set, so a user whose `HOME` differs
|
|
496
|
+
* would get a provider block in a store Command Code never opens. Only the OS-default
|
|
497
|
+
* home is replaced: a caller that injects a different root (a test, the writer's
|
|
498
|
+
* `input.home`) keeps it, and a blank or relative value falls back to `home`.
|
|
499
|
+
*/
|
|
500
|
+
export function commandCodeUserHome(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
501
|
+
if (platform !== "win32" || home !== homedir()) return home;
|
|
502
|
+
const value = env.HOME ?? env.USERPROFILE;
|
|
503
|
+
return typeof value === "string" && value.trim() && win32.isAbsolute(value) ? value : home;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Command Code's provider store directory — `.commandcode` under the home the client
|
|
508
|
+
* resolves (see {@link commandCodeUserHome}), with no override.
|
|
509
|
+
*
|
|
510
|
+
* There is deliberately no `COMMANDCODE_HOME` here, unlike the other clients in this
|
|
511
|
+
* file: `grep -c COMMANDCODE_HOME` over the shipped `dist/cli.mjs` and
|
|
512
|
+
* `dist/index.mjs` returns 0. An override we honour but the client ignores is worse
|
|
513
|
+
* than none: `enable` would report success and write a provider block at a path no
|
|
514
|
+
* Command Code process ever opens, and the user would see an empty model list with no
|
|
515
|
+
* error anywhere. An apply against a relocated home is instead refused by `detectDir`
|
|
516
|
+
* not existing.
|
|
517
|
+
*/
|
|
518
|
+
export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
519
|
+
return join(commandCodeUserHome(env, home, platform), ".commandcode");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
523
|
+
return join(commandCodeHomeDir(env, home, platform), "providers.json");
|
|
524
|
+
}
|
|
525
|
+
|
|
477
526
|
/**
|
|
478
527
|
* The provider store a current ZCode reads, which is NOT the file above.
|
|
479
528
|
*
|
|
@@ -1487,6 +1536,20 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1487
1536
|
// remote binds refuse — same reasoning as MCode.
|
|
1488
1537
|
loopbackOnly: true,
|
|
1489
1538
|
},
|
|
1539
|
+
commandcode: {
|
|
1540
|
+
id: "commandcode",
|
|
1541
|
+
filename: "providers.json",
|
|
1542
|
+
destination: env => commandCodeConfigPath(env),
|
|
1543
|
+
// No env var exists behind this integration: buildCommandCodeClientConfig
|
|
1544
|
+
// writes `apiKey: false`, the documented keyless form for a loopback endpoint.
|
|
1545
|
+
apiKeyEnv: "",
|
|
1546
|
+
exportHint: "The provider is written with `apiKey: false` — Command Code treats a keyless loopback endpoint as already authenticated. No key is stored or referenced.",
|
|
1547
|
+
build: buildCommandCodeClientConfig,
|
|
1548
|
+
format: "json",
|
|
1549
|
+
summarize: summarizeCommandCode,
|
|
1550
|
+
buildContribution: buildCommandCodeContribution,
|
|
1551
|
+
loopbackOnly: true,
|
|
1552
|
+
},
|
|
1490
1553
|
prime: {
|
|
1491
1554
|
id: "prime",
|
|
1492
1555
|
filename: "prime-models.json",
|
package/src/clients/lazycodex.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Detection of Codex-based omo (LazyCodex), the variant that reads Codex role files and
|
|
3
|
-
* `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
|
|
3
|
+
* `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
|
|
4
4
|
*
|
|
5
5
|
* "omo" names three products. Pi-based omo (senpi) owns `~/.omo/agent` and is the omo file
|
|
6
6
|
* integration in `src/integrations/registry.ts`; OpenCode-based omo (oh-my-opencode) keeps its
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* omo (Codex / LazyCodex)'s per-role model setting, `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
|
|
2
|
+
* omo (Codex / LazyCodex)'s per-role model setting, `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
|
|
3
|
+
* The section name carries its brackets: LazyCodex reads `[codex]`, and its strict loader drops a
|
|
4
|
+
* bare `codex` key as unknown, so a pick written there never reaches a role.
|
|
3
5
|
*
|
|
4
6
|
* LazyCodex 5.1.1 and later reads it, and callers reach this only after `detectLazyCodex`
|
|
5
7
|
* says LazyCodex is installed. The file is omo's, so this writes only on an explicit
|
|
@@ -30,6 +32,17 @@ export function omoJsoncPath(env: NodeJS.ProcessEnv = process.env, home: string
|
|
|
30
32
|
|
|
31
33
|
type JsonObject = Record<string, unknown>;
|
|
32
34
|
|
|
35
|
+
const CODEX_SECTION = "[codex]";
|
|
36
|
+
|
|
37
|
+
/** The reasoning levels LazyCodex accepts for a role; it names Codex's `none` `off`. */
|
|
38
|
+
const LAZYCODEX_REASONING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
39
|
+
|
|
40
|
+
/** The `reasoning` value LazyCodex reads for a Codex effort, or null when it has no such level. */
|
|
41
|
+
export function omoReasoningFor(effort: string): string | null {
|
|
42
|
+
const level = effort === "none" ? "off" : effort;
|
|
43
|
+
return LAZYCODEX_REASONING.has(level) ? level : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
function isObject(value: unknown): value is JsonObject {
|
|
34
47
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
48
|
}
|
|
@@ -71,7 +84,7 @@ function load(path: string): Loaded {
|
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
function agentsOf(doc: JsonObject): JsonObject | null | undefined {
|
|
74
|
-
const codex = doc
|
|
87
|
+
const codex = doc[CODEX_SECTION];
|
|
75
88
|
if (codex === undefined) return undefined;
|
|
76
89
|
if (!isObject(codex)) return null;
|
|
77
90
|
const agents = codex.agents;
|
|
@@ -104,21 +117,33 @@ function serialize(text: string, doc: JsonObject): string {
|
|
|
104
117
|
return text.startsWith("\ufeff") ? `\ufeff${out}` : out;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
|
|
120
|
+
/**
|
|
121
|
+
* `reasoning` left undefined keeps the entry's level; null removes it, so a level LazyCodex cannot
|
|
122
|
+
* express does not leave a stale one behind for its next install to apply.
|
|
123
|
+
*/
|
|
124
|
+
export function writeOmoRoleModel(
|
|
125
|
+
role: string,
|
|
126
|
+
model: string,
|
|
127
|
+
path: string = omoJsoncPath(),
|
|
128
|
+
reasoning?: string | null,
|
|
129
|
+
): OmoRoleModelWriteStatus {
|
|
108
130
|
const loaded = load(path);
|
|
109
131
|
if (loaded.kind !== "document") return loaded.kind === "comments" ? "skipped_comments" : loaded.kind;
|
|
110
132
|
const { doc, text } = loaded;
|
|
111
133
|
// Only an absent key is created; an explicit null is a value the reader calls invalid.
|
|
112
|
-
const codex = doc
|
|
134
|
+
const codex = doc[CODEX_SECTION] === undefined ? {} : doc[CODEX_SECTION];
|
|
113
135
|
if (!isObject(codex)) return "invalid";
|
|
114
136
|
const agents = codex.agents === undefined ? {} : codex.agents;
|
|
115
137
|
if (!isObject(agents)) return "invalid";
|
|
116
138
|
const entry = agents[role] === undefined ? {} : agents[role];
|
|
117
139
|
if (!isObject(entry)) return "invalid";
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
const next: JsonObject = { ...entry, model };
|
|
141
|
+
if (reasoning === null) delete next.reasoning;
|
|
142
|
+
else if (reasoning !== undefined) next.reasoning = reasoning;
|
|
143
|
+
if (entry.model === model && entry.reasoning === next.reasoning) return "unchanged";
|
|
144
|
+
agents[role] = next;
|
|
120
145
|
codex.agents = agents;
|
|
121
|
-
doc
|
|
146
|
+
doc[CODEX_SECTION] = codex;
|
|
122
147
|
assertIntegrationWriteOwnership(path);
|
|
123
148
|
atomicWriteFileNoFollowUnclaimed(path, serialize(text, doc));
|
|
124
149
|
return "written";
|
|
@@ -252,6 +252,13 @@ export function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
export interface CodexAuthAccountsListOptions {
|
|
256
|
+
/** Finish deferred registration; only a dashboard session carries that inference consent. */
|
|
257
|
+
validatePending?: boolean;
|
|
258
|
+
/** An operator's explicit refresh command, which may retry a grant already recorded as dead. */
|
|
259
|
+
explicitRefresh?: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
255
262
|
export interface CodexAuthAccountsSnapshot {
|
|
256
263
|
accounts: CodexAuthAccountDto[];
|
|
257
264
|
mainIdentityGeneration: number;
|
|
@@ -260,7 +267,7 @@ export interface CodexAuthAccountsSnapshot {
|
|
|
260
267
|
export async function listCodexAuthAccountsSnapshot(
|
|
261
268
|
config: OcxConfig,
|
|
262
269
|
forceRefresh = false,
|
|
263
|
-
options:
|
|
270
|
+
options: CodexAuthAccountsListOptions = {},
|
|
264
271
|
): Promise<CodexAuthAccountsSnapshot> {
|
|
265
272
|
const runtimeConfig = getRuntimeConfig(config);
|
|
266
273
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
@@ -281,7 +288,8 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
281
288
|
// identity when the switch is on and nothing has been observed yet.
|
|
282
289
|
const creditsIdentity = runtimeConfig.showCodexCredits === true ? poolQuotaHistoryIdentity(account.id) ?? null : null;
|
|
283
290
|
const creditsUnobserved = creditsIdentity !== null && !hasCodexCreditsObservation(account.id, creditsIdentity);
|
|
284
|
-
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
|
|
291
|
+
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
|
|
292
|
+
options.validatePending === true, undefined, undefined, options.explicitRefresh === true);
|
|
285
293
|
} catch (error) {
|
|
286
294
|
if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
|
|
287
295
|
quotaResult = {
|
|
@@ -447,7 +455,7 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
447
455
|
export async function listCodexAuthAccounts(
|
|
448
456
|
config: OcxConfig,
|
|
449
457
|
forceRefresh = false,
|
|
450
|
-
options:
|
|
458
|
+
options: CodexAuthAccountsListOptions = {},
|
|
451
459
|
): Promise<CodexAuthAccountDto[]> {
|
|
452
460
|
return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
|
|
453
461
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { getConfigPath, initializePersistedConfigIfMissing, mutatePersistedConfig } from "../../config";
|
|
3
|
+
import { mergeConfigDefaults, readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
|
|
4
|
+
import { adoptPersistedCodexAccountSelection, claudeCodeBaselineArmed } from "../../config/live-reconcile";
|
|
5
|
+
import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig } from "../../config/rebase-provenance";
|
|
6
|
+
import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
7
|
+
import { isCodexAccountPaused } from "../account-pause";
|
|
8
|
+
import { readCodexAccountRecord } from "../account-store";
|
|
9
|
+
import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
|
|
10
|
+
import { jsonResponse } from "./http";
|
|
11
|
+
|
|
12
|
+
class SelectionRejected extends Error {
|
|
13
|
+
constructor(message: string, readonly status: number) { super(message); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateSelection(config: OcxConfig, accountId: string | null): void {
|
|
17
|
+
if (accountId === null) return;
|
|
18
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(config.codexAccounts)) {
|
|
19
|
+
throw new SelectionRejected("Remove the legacy __main__ pool row before selecting the Desktop account", 409);
|
|
20
|
+
}
|
|
21
|
+
if (isCodexAccountPaused(config, accountId)) throw new SelectionRejected("Account is paused", 409);
|
|
22
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return;
|
|
23
|
+
if (!isValidCodexAccountId(accountId)) throw new SelectionRejected("Invalid account id format", 400);
|
|
24
|
+
if (!(config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === accountId)) {
|
|
25
|
+
throw new SelectionRejected("Account not found", 400);
|
|
26
|
+
}
|
|
27
|
+
if (readCodexAccountRecord(accountId)?.codexValidationPending) {
|
|
28
|
+
throw new SelectionRejected("Account validation is pending. Refresh quota after recovery to validate it.", 409);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applySelection(config: OcxConfig, accountId: string | null): boolean {
|
|
33
|
+
const changed = config.activeCodexAccountId !== (accountId ?? undefined)
|
|
34
|
+
|| config.activeCodexAccountPinned !== (accountId ?? undefined);
|
|
35
|
+
if (accountId === null) {
|
|
36
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountId");
|
|
37
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
|
|
38
|
+
} else {
|
|
39
|
+
config.activeCodexAccountId = accountId;
|
|
40
|
+
config.activeCodexAccountPinned = accountId;
|
|
41
|
+
}
|
|
42
|
+
return changed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A manual selection is an explicit command, even when equal to the live merge baseline. */
|
|
46
|
+
export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
|
|
47
|
+
const unknown = () => jsonResponse({
|
|
48
|
+
error: "Account selection could not be confirmed; reload settings before retrying",
|
|
49
|
+
code: "account_selection_unavailable",
|
|
50
|
+
}, 409);
|
|
51
|
+
const configPath = getConfigPath();
|
|
52
|
+
const sourcePath = fileBackedConfigPath(config);
|
|
53
|
+
if (sourcePath !== undefined && sourcePath !== configPath) return unknown();
|
|
54
|
+
let committed: OcxConfig;
|
|
55
|
+
let warning = false;
|
|
56
|
+
try {
|
|
57
|
+
// Do not activate a newly disk-only account that the running proxy has not adopted.
|
|
58
|
+
validateSelection(config, accountId);
|
|
59
|
+
const before = readConfigFileSnapshot();
|
|
60
|
+
if (before.diagnostics.source === "default" && !fileBackedConfigPath(config) && !claudeCodeBaselineArmed(config)) {
|
|
61
|
+
// Preserve transient/initial callers without recreating a deleted live configuration.
|
|
62
|
+
// The create-only owner cannot replace a competing writer's newly published config.
|
|
63
|
+
const normalized = validateConfigCandidate(mergeConfigDefaults(config));
|
|
64
|
+
if (!normalized.ok) return unknown();
|
|
65
|
+
const initial = normalized.config;
|
|
66
|
+
applySelection(initial, accountId);
|
|
67
|
+
initializePersistedConfigIfMissing(initial);
|
|
68
|
+
}
|
|
69
|
+
const result = mutatePersistedConfig(persisted => {
|
|
70
|
+
// This callback is re-run under the mutation lock immediately before publication.
|
|
71
|
+
// A removed or paused account must not be resurrected by a stale UI snapshot.
|
|
72
|
+
validateSelection(config, accountId);
|
|
73
|
+
validateSelection(persisted, accountId);
|
|
74
|
+
return { changed: applySelection(persisted, accountId), value: persisted };
|
|
75
|
+
});
|
|
76
|
+
if (result.status === "unavailable") return unknown();
|
|
77
|
+
committed = result.value;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
|
|
80
|
+
// Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
|
|
81
|
+
// durable selection instead of rolling live state back behind an already published write.
|
|
82
|
+
const snapshot = readConfigFileSnapshot();
|
|
83
|
+
if (snapshot.raw === undefined) return unknown();
|
|
84
|
+
try {
|
|
85
|
+
const strict = validateConfigCandidate(JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")));
|
|
86
|
+
if (!strict.ok) return unknown();
|
|
87
|
+
validateSelection(config, accountId);
|
|
88
|
+
validateSelection(strict.config, accountId);
|
|
89
|
+
if (strict.config.activeCodexAccountId !== (accountId ?? undefined)
|
|
90
|
+
|| strict.config.activeCodexAccountPinned !== (accountId ?? undefined)) return unknown();
|
|
91
|
+
committed = strict.config;
|
|
92
|
+
warning = true;
|
|
93
|
+
} catch { return unknown(); }
|
|
94
|
+
}
|
|
95
|
+
adoptPersistedCodexAccountSelection(config, committed);
|
|
96
|
+
recordFileBackedConfig(config, configPath);
|
|
97
|
+
// No failed save may clear affinity, spend the manual preference, or move the RR cursor.
|
|
98
|
+
resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
|
|
99
|
+
const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
|
|
100
|
+
return jsonResponse({
|
|
101
|
+
ok: true,
|
|
102
|
+
activeCodexAccountId: config.activeCodexAccountId ?? null,
|
|
103
|
+
appliesImmediately: true,
|
|
104
|
+
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
105
|
+
...(warning ? { warning: "config_bookkeeping_failed" } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -146,6 +146,11 @@ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxCon
|
|
|
146
146
|
return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export interface MainAccountInfoProbeOptions {
|
|
150
|
+
/** Metadata-only renewal never sets or clears the traffic reauth quarantine. */
|
|
151
|
+
passive?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
|
|
150
155
|
|
|
151
156
|
export async function retryMainAccountInfoIfIdentityChanged(
|
|
@@ -154,12 +159,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
|
|
|
154
159
|
nativeMainLease: AdmissionLease,
|
|
155
160
|
explicitRefresh: boolean,
|
|
156
161
|
paced: boolean,
|
|
162
|
+
options: MainAccountInfoProbeOptions = {},
|
|
157
163
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
158
164
|
const currentAccountId = getMainChatgptAccountId();
|
|
159
165
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
160
166
|
reconcileMainCodexAccountRuntimeState();
|
|
161
167
|
return retriesRemaining > 0
|
|
162
|
-
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
|
|
168
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced, options)
|
|
163
169
|
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -171,6 +177,7 @@ export async function fetchMainAccountInfoAttempt(
|
|
|
171
177
|
explicitRefresh: boolean = forceRefresh,
|
|
172
178
|
postReset = false,
|
|
173
179
|
config?: OcxConfig,
|
|
180
|
+
options: MainAccountInfoProbeOptions = {},
|
|
174
181
|
): Promise<MainAccountInfoFetchResult> {
|
|
175
182
|
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
176
183
|
if (!nativeMainLease) {
|
|
@@ -184,7 +191,7 @@ export async function fetchMainAccountInfoAttempt(
|
|
|
184
191
|
try {
|
|
185
192
|
const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
|
|
186
193
|
const operation = async () => ({
|
|
187
|
-
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
|
|
194
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced, options),
|
|
188
195
|
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
189
196
|
});
|
|
190
197
|
if (nativeMainSharedClaimHeld) return await operation();
|
|
@@ -225,6 +232,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
225
232
|
/** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
|
|
226
233
|
postReset = false,
|
|
227
234
|
paced = true,
|
|
235
|
+
options: MainAccountInfoProbeOptions = {},
|
|
228
236
|
): Promise<MainAccountInfoFetchResult> {
|
|
229
237
|
const writerGeneration = captureConfigGeneration();
|
|
230
238
|
reconcileMainCodexAccountRuntimeState();
|
|
@@ -287,7 +295,16 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
287
295
|
const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
|
|
288
296
|
if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
289
297
|
credentialChecked: true, hasCredential: true };
|
|
290
|
-
|
|
298
|
+
// Single-flight shares evidence, but each caller retains its own auth behavior.
|
|
299
|
+
if (!options.passive && admission.result.terminalAuthFailure) {
|
|
300
|
+
const diagnosticStillLive = admission.result.quotaRefreshGeneration !== undefined
|
|
301
|
+
&& isMainAccountIdentityGenerationLive(admission.result.quotaRefreshGeneration);
|
|
302
|
+
clearMainAccountInfoCache();
|
|
303
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
304
|
+
return { ...admission.result, ...(diagnosticStillLive
|
|
305
|
+
? { quotaRefreshGeneration: captureMainAccountIdentityGeneration() } : {}) };
|
|
306
|
+
}
|
|
307
|
+
if (!options.passive && explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
|
|
291
308
|
|| admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
292
309
|
return admission.result;
|
|
293
310
|
}
|
|
@@ -297,13 +314,13 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
297
314
|
if (!resp.ok) {
|
|
298
315
|
const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
299
316
|
const terminalAuthFailure = authFailure.terminal;
|
|
300
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
317
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
301
318
|
if (retried) return retried;
|
|
302
319
|
if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
|
|
303
320
|
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
304
321
|
credentialChecked: true, hasCredential: true };
|
|
305
322
|
}
|
|
306
|
-
if (terminalAuthFailure) {
|
|
323
|
+
if (terminalAuthFailure && !options.passive) {
|
|
307
324
|
// Account for this attempt's own synchronous invalidation, never prior external drift.
|
|
308
325
|
const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
|
|
309
326
|
clearMainAccountInfoCache();
|
|
@@ -324,7 +341,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
324
341
|
quotaPhase = "body";
|
|
325
342
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
326
343
|
quotaPhase = "publish";
|
|
327
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
344
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
328
345
|
if (retried) return retried;
|
|
329
346
|
quotaPhase = "decode";
|
|
330
347
|
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
@@ -376,7 +393,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
376
393
|
// never settled and the dashboard kept showing nothing — the symptom #327 reported.
|
|
377
394
|
// An explicit refresh is an operator asking to re-evaluate, normally right after
|
|
378
395
|
// signing in again, so it stays authoritative.
|
|
379
|
-
if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
396
|
+
if (explicitRefresh && !options.passive) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
380
397
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
381
398
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
382
399
|
setMainAccountPlan(result.plan);
|
|
@@ -398,7 +415,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
398
415
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
399
416
|
};
|
|
400
417
|
} catch (error) {
|
|
401
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
418
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
402
419
|
if (retried) return retried;
|
|
403
420
|
if (!credentialIsCurrent()) {
|
|
404
421
|
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|