@bitkyc08/opencodex 2.86.0 → 2.88.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/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +46 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/cli/provider.ts
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, readConfigAdmissionSnapshot, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
|
|
12
12
|
import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
|
|
13
13
|
import { hasHelpFlag, printSubcommandUsage } from "./help";
|
|
14
14
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
15
15
|
import { providerConfigSeed } from "../providers/derive";
|
|
16
16
|
import { assertAnthropicInstanceLoginConfig } from "../oauth/store-anthropic-instance";
|
|
17
17
|
import { dropProviderCustomModels } from "../providers/provider-id-rewrite";
|
|
18
|
+
import { cleanupRemovedGcpCredentials, commitProviderGcpCredentialRemoval, providerGcpCredentialMarkers } from "../providers/gcp-credential-cleanup";
|
|
19
|
+
import { providerCredentialInputError } from "../providers/api-keys";
|
|
18
20
|
import type { OcxProviderConfig } from "../types";
|
|
19
21
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
20
22
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -223,6 +225,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
|
|
|
223
225
|
if (registryEntry) {
|
|
224
226
|
provConfig = providerConfigSeed(registryEntry);
|
|
225
227
|
if (apiKey) {
|
|
228
|
+
const candidateError = providerCredentialInputError({ apiKey });
|
|
229
|
+
if (candidateError) {
|
|
230
|
+
console.error(`Error: ${candidateError}`);
|
|
231
|
+
process.exitCode = 2;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
226
234
|
if ((authMode ?? registryEntry.authKind) === "forward") {
|
|
227
235
|
console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`);
|
|
228
236
|
} else if ((authMode ?? registryEntry.authKind) === "oauth") {
|
|
@@ -249,6 +257,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
|
|
|
249
257
|
}
|
|
250
258
|
|
|
251
259
|
if (responsesPath !== undefined) provConfig.responsesPath = responsesPath;
|
|
260
|
+
const credentialError = providerCredentialInputError(provConfig);
|
|
261
|
+
if (credentialError) {
|
|
262
|
+
console.error(`Error: ${credentialError}`);
|
|
263
|
+
process.exitCode = 2;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
252
266
|
if (authMode !== undefined) provConfig.authMode = authMode as OcxProviderConfig["authMode"];
|
|
253
267
|
if (name === "anthropic2") {
|
|
254
268
|
if (provConfig.adapter !== "anthropic" || provConfig.authMode !== "oauth") delete provConfig.anthropicOAuthInstance;
|
|
@@ -328,12 +342,17 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
|
|
|
328
342
|
}
|
|
329
343
|
const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
|
|
330
344
|
let markerCollision = false;
|
|
345
|
+
let removedCredentials: string[] = [];
|
|
331
346
|
withConfigMutationLockSync(() => {
|
|
332
347
|
if (provConfig.anthropicOAuthInstance) {
|
|
333
348
|
try { assertAnthropicInstanceLoginConfig(config, name); }
|
|
334
349
|
catch { markerCollision = true; return; }
|
|
335
350
|
}
|
|
336
351
|
initializeProviderModelSelection(name, provConfig, existingProvider, config);
|
|
352
|
+
const durable = readConfigAdmissionSnapshot();
|
|
353
|
+
const durableProvider = durable.kind === "read" && durable.diagnostics.source === "file"
|
|
354
|
+
&& durable.diagnostics.error === null ? durable.diagnostics.config.providers[name] : undefined;
|
|
355
|
+
removedCredentials = [config.providers[name], durableProvider].flatMap(provider => provider ? providerGcpCredentialMarkers(provider) : []);
|
|
337
356
|
config.providers[name] = provConfig;
|
|
338
357
|
if (setDefault) config.defaultProvider = name;
|
|
339
358
|
validateAndSave(config);
|
|
@@ -344,6 +363,7 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
|
|
|
344
363
|
return;
|
|
345
364
|
}
|
|
346
365
|
|
|
366
|
+
cleanupRemovedGcpCredentials(removedCredentials);
|
|
347
367
|
let sync: LocalSyncResult | undefined;
|
|
348
368
|
if (wantsSync) {
|
|
349
369
|
try {
|
|
@@ -440,10 +460,12 @@ function handleRemove(args: string[]): void {
|
|
|
440
460
|
process.exit(1);
|
|
441
461
|
}
|
|
442
462
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
463
|
+
const droppedCustomModels = commitProviderGcpCredentialRemoval(config, name, () => {
|
|
464
|
+
delete config.providers[name];
|
|
465
|
+
const dropped = dropProviderCustomModels(config, name);
|
|
466
|
+
validateAndSave(config);
|
|
467
|
+
return dropped;
|
|
468
|
+
});
|
|
447
469
|
|
|
448
470
|
if (wantsJson) {
|
|
449
471
|
console.log(JSON.stringify({
|
package/src/cli/registry.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface CliCommandEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "codex-provider",
|
|
13
|
+
usage: "ocx codex-provider <status|allow <id> [--yes]|revoke <id>>",
|
|
14
|
+
summary: "Explicitly allow or revoke local routing management for a custom Codex provider.",
|
|
15
|
+
details: ["Consent is bound to the selected CODEX_HOME and provider ID. Only base_url is managed; transport settings and history are preserved."],
|
|
16
|
+
},
|
|
11
17
|
{
|
|
12
18
|
name: "message",
|
|
13
19
|
usage: "ocx message <sessions|send> [options]",
|
|
@@ -178,7 +184,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
178
184
|
summary: "Fetch provider models and inject them into Codex config.",
|
|
179
185
|
details: [
|
|
180
186
|
"After writing the catalog, warns if long-lived Codex app-server processes are still running.",
|
|
181
|
-
"--restart-codex restarts the app-servers AND fully quits and relaunches the Codex desktop app on macOS, Linux and Windows,
|
|
187
|
+
"--restart-codex restarts the app-servers AND fully quits and relaunches the Codex desktop app on macOS, Linux and Windows, after catalog/cache writes or a committed configuration change. It ends live conversations.",
|
|
182
188
|
"--restart-app-server-only keeps the narrow behaviour: SIGTERM to matching app-server / code-mode-host processes, desktop app left running. It wins over --restart-codex when both are given.",
|
|
183
189
|
"--restart-desktop-app is a deprecated alias of --restart-codex and prints a notice.",
|
|
184
190
|
],
|
|
@@ -226,9 +232,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
226
232
|
},
|
|
227
233
|
{
|
|
228
234
|
name: "login",
|
|
229
|
-
usage: "ocx login <provider>",
|
|
235
|
+
usage: "ocx login <provider> [--headless]",
|
|
230
236
|
summary: "OAuth or API-key login for a provider.",
|
|
231
237
|
details: [
|
|
238
|
+
"--headless (anthropic, anthropic2): no browser and no local port; approve in any browser and paste the displayed code.",
|
|
232
239
|
"Codex/ChatGPT: ocx login codex runs the Codex account-pool login (same flow as ocx account login codex).",
|
|
233
240
|
"That one needs a running proxy; the OAuth and API-key providers log in locally.",
|
|
234
241
|
"'chatgpt' and 'openai' are the same route; an OpenAI platform API key is 'ocx login openai-apikey'.",
|
|
@@ -336,6 +343,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
336
343
|
"add-key <provider> [--label <label>] Add a key read only from piped stdin.",
|
|
337
344
|
"pool <provider> Read/edit runtime-supported pool policy.",
|
|
338
345
|
"credits openai ... Explicit paid-credit-after-limit policy; separate from displaying balances.",
|
|
346
|
+
"use-remaining openai <id|main> <on|off|status> Use included quota up to its limit.",
|
|
339
347
|
"quota-activation openai Enable/disable one account reset-window refresh policy.",
|
|
340
348
|
"anthropic-reset-grants Read grant status only; consumption remains in the dashboard.",
|
|
341
349
|
"login supports explicit browser/add-account options only for applicable flows; human verification remains required.",
|
|
@@ -444,11 +452,21 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
444
452
|
"`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
|
|
445
453
|
],
|
|
446
454
|
},
|
|
447
|
-
{
|
|
455
|
+
{
|
|
456
|
+
name: "logs",
|
|
457
|
+
usage: "ocx logs [filters] [--follow] [--json|--jsonl|--events]; ocx logs history [selectors] [--cursor <token>] [--json|--jsonl]; ocx logs get <request-id> [--json]",
|
|
458
|
+
summary: "Read or follow request logs.",
|
|
459
|
+
details: [
|
|
460
|
+
"Default reads the recent ring. `history` pages the derived index with a resumable cursor; `get` reads one canonical row; `explain` is the route-decision view.",
|
|
461
|
+
],
|
|
462
|
+
},
|
|
448
463
|
{
|
|
449
464
|
name: "usage",
|
|
450
|
-
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--json]",
|
|
465
|
+
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--top <1-1000>] [--json]; ocx usage compare [--range <today|1d|7d|30d>] [--json]",
|
|
451
466
|
summary: "Report token usage and estimated cost (alias of ocx observe usage).",
|
|
467
|
+
details: [
|
|
468
|
+
"`compare` reads the current named range and the previous equal window; `--range all` is refused.",
|
|
469
|
+
],
|
|
452
470
|
},
|
|
453
471
|
{
|
|
454
472
|
name: "storage",
|
|
@@ -486,13 +504,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
486
504
|
},
|
|
487
505
|
{
|
|
488
506
|
name: "export",
|
|
489
|
-
usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
|
|
490
|
-
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder) wired to the running proxy.",
|
|
507
|
+
usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid|gentle-shell> [--json] [--out <path>] [--force]",
|
|
508
|
+
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder, Gentle Shell) wired to the running proxy.",
|
|
491
509
|
details: [
|
|
492
510
|
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
493
511
|
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|
|
494
512
|
"Cline exports a two-document bundle: settings for providers.json and catalog for sibling models.json. Use integration client enable --client cline for a journaled write.",
|
|
495
513
|
"Droid exports documented customModels for settings.json; use integration client enable --client droid for a journaled write.",
|
|
514
|
+
"Gentle Shell uses its persistent selected home; use integration client enable --client gentle-shell for a journaled write, or Pi for link/shared homes.",
|
|
496
515
|
"The config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.",
|
|
497
516
|
"The destination path is printed for merging by hand — ocx never writes your real client config.",
|
|
498
517
|
],
|
|
@@ -516,14 +535,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
516
535
|
{
|
|
517
536
|
name: "system",
|
|
518
537
|
usage: "ocx system <status|health|settings|startup|diagnostics|sync|codex-app-server|codex-restart|update|codex-cli-update> ...",
|
|
519
|
-
summary: "Manage headless runtime settings, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection.",
|
|
538
|
+
summary: "Manage headless runtime settings, health, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection and planning.",
|
|
520
539
|
details: [
|
|
521
540
|
"system update manages OpenCodex itself.",
|
|
522
541
|
"system health reads authenticated service and spend-ledger diagnostics; root health remains liveness.",
|
|
523
542
|
"ocx system codex-cli-update check [--json]",
|
|
524
543
|
"ocx system codex-cli-update attest [--json]",
|
|
525
544
|
"ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]",
|
|
526
|
-
"
|
|
545
|
+
"ocx system codex-cli-update plan [--channel latest] [--json]",
|
|
546
|
+
"check and attest make no package-registry request, do not execute Codex or npm, and write nothing. plan adds a registry query and a process-table read and mutates no application state (temporary isolated npm files only, removed best-effort).",
|
|
527
547
|
],
|
|
528
548
|
},
|
|
529
549
|
{
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -279,13 +279,15 @@ export function takeBooleanOption(args: string[], flag: string): boolean | undef
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/** Parse a decimal safe integer, allowing separators only between digits. */
|
|
282
|
-
export function takeIntegerOption(args: string[], flag: string, options: { min?: number } = {}): number | undefined {
|
|
282
|
+
export function takeIntegerOption(args: string[], flag: string, options: { min?: number; max?: number } = {}): number | undefined {
|
|
283
283
|
const raw = takeOption(args, flag);
|
|
284
284
|
if (raw === undefined) return undefined;
|
|
285
285
|
const decimal = raw.trim();
|
|
286
286
|
const value = /^[+-]?\d+(?:[_,]\d+)*$/.test(decimal) ? Number(decimal.replace(/[_,]/g, "")) : NaN;
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
const { min, max } = options;
|
|
288
|
+
if (!Number.isSafeInteger(value) || value < (min ?? Number.MIN_SAFE_INTEGER) || value > (max ?? Number.MAX_SAFE_INTEGER)) {
|
|
289
|
+
const bound = min !== undefined && max !== undefined ? ` between ${min} and ${max}` : min !== undefined ? ` >= ${min}` : max !== undefined ? ` <= ${max}` : "";
|
|
290
|
+
throw new CliUsageError(`${flag} must be an integer${bound}`);
|
|
289
291
|
}
|
|
290
292
|
return value;
|
|
291
293
|
}
|
|
@@ -28,6 +28,7 @@ const USAGE = `Usage:
|
|
|
28
28
|
ocx system codex-cli-update check [--json]
|
|
29
29
|
ocx system codex-cli-update attest [--json]
|
|
30
30
|
ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
|
|
31
|
+
ocx system codex-cli-update plan [--channel latest] [--json]
|
|
31
32
|
ocx system update check [--channel <latest|preview>] [--json]
|
|
32
33
|
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
|
|
33
34
|
ocx system update status <job-id> [--json]
|
|
@@ -270,24 +271,55 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
|
|
|
270
271
|
const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
|
|
271
272
|
printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
|
|
272
273
|
} else if (sub === "codex-restart") {
|
|
273
|
-
// --yes required: this
|
|
274
|
-
//
|
|
274
|
+
// --yes required: this interrupts running Codex turns, and when the desktop picker predates
|
|
275
|
+
// the catalog it also quits and relaunches the app, which can discard unsaved client state.
|
|
275
276
|
const args = [...rest];
|
|
276
277
|
const wantsJson = takeFlag(args, "--json");
|
|
277
278
|
const yes = takeFlag(args, "--yes");
|
|
278
279
|
if (!yes) throw new CliUsageError(
|
|
279
|
-
"system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost
|
|
280
|
+
"system codex-restart requires --yes: this restarts the Codex app-servers, interrupting running turns, and when the desktop app's model picker predates the current catalog (or that cannot be determined) it fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost",
|
|
280
281
|
USAGE,
|
|
281
282
|
);
|
|
282
283
|
rejectArgs(args, USAGE);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
wantsJson,
|
|
286
|
-
["Codex desktop app and app-server restart requested. Unsaved composer drafts, model-picker selections, and pending approval prompts may be lost."],
|
|
287
|
-
);
|
|
284
|
+
const result = await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps);
|
|
285
|
+
printData(result, wantsJson, codexRestartLines(result));
|
|
288
286
|
} else if (sub === "update") await update(rest, deps);
|
|
289
287
|
else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
|
|
290
288
|
});
|
|
291
289
|
}
|
|
292
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Operator text for a codex-restart outcome. It reports what happened to the desktop app,
|
|
293
|
+
* because whether unsaved client state survived is the one thing the operator needs to know.
|
|
294
|
+
*/
|
|
295
|
+
export function codexRestartLines(result: unknown): string[] {
|
|
296
|
+
const view = recordValue(result);
|
|
297
|
+
const desktop = recordValue(view?.desktopApp);
|
|
298
|
+
const reason = typeof desktop?.reason === "string" ? desktop.reason : undefined;
|
|
299
|
+
// Only a reported `stale` reading may claim the picker predated the catalog; an unreadable
|
|
300
|
+
// reading also takes the full restart, and an older proxy reports no reading at all.
|
|
301
|
+
const why = desktop?.pickerState === "stale"
|
|
302
|
+
? "its model picker predated the current catalog"
|
|
303
|
+
: "its model picker could not be confirmed current";
|
|
304
|
+
const stopped = Array.isArray(view?.stopped) ? view.stopped.length : 0;
|
|
305
|
+
const appServers = view?.code === "stopped"
|
|
306
|
+
? `Stopped ${stopped} Codex app-server(s); their owners start replacements.`
|
|
307
|
+
: view?.code === "partially_stopped"
|
|
308
|
+
? "Some Codex app-servers did not exit; stop them manually if Codex stays stale."
|
|
309
|
+
: view?.code === "enumeration_unavailable"
|
|
310
|
+
? "Could not list Codex app-servers, so none were stopped."
|
|
311
|
+
: "No Codex app-server needed stopping.";
|
|
312
|
+
if (reason === "picker_current") {
|
|
313
|
+
return [appServers, "The Codex desktop app was left running: its model picker already shows the current catalog, so unsaved drafts and pending approvals were kept."];
|
|
314
|
+
}
|
|
315
|
+
if (reason === "no_targets") return [appServers, "The Codex desktop app is not running."];
|
|
316
|
+
if (desktop?.relaunch === "started") {
|
|
317
|
+
return [appServers, `The Codex desktop app was quit and relaunched because ${why}. Unsaved composer drafts, model-picker selections, and pending approval prompts may have been lost.`];
|
|
318
|
+
}
|
|
319
|
+
if (desktop?.attempted === true) {
|
|
320
|
+
return [appServers, `The Codex desktop app restart did not complete (${reason ?? "no outcome reported"}). Unsaved composer drafts, model-picker selections, and pending approval prompts may have been lost.`];
|
|
321
|
+
}
|
|
322
|
+
return [appServers, `The Codex desktop app was not restarted (${reason ?? "no outcome reported"}).`];
|
|
323
|
+
}
|
|
324
|
+
|
|
293
325
|
export const SYSTEM_USAGE = USAGE;
|
|
@@ -5,9 +5,13 @@ import { assertClientLifecycleHeld, withClientLifecycle } from "../client/lifecy
|
|
|
5
5
|
import { inspectRemoteDesktopCleanup, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
|
|
6
6
|
import { removeOwnedConfigState, type ConfigRemovalResult } from "../lib/config-ownership";
|
|
7
7
|
import { windowsSecretAclReapPendingAtOrBelow } from "../lib/windows-secret-acl";
|
|
8
|
+
import { supportsDesktopPicker } from "../claude/desktop-picker-platform";
|
|
8
9
|
import { sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
9
10
|
import { cleanupOwnedIntegrationsBeforeUninstall } from "./uninstall-integrations";
|
|
10
11
|
|
|
12
|
+
/** Outcome of retiring the Desktop picker profile and every picker root this install trusted. */
|
|
13
|
+
export interface PickerUninstallCleanup { ok: boolean; residual?: string[] }
|
|
14
|
+
|
|
11
15
|
export interface UninstallClientStateDeps {
|
|
12
16
|
readConnection: typeof readClientConnectionState;
|
|
13
17
|
inspectDesktop: typeof inspectRemoteDesktopCleanup;
|
|
@@ -18,6 +22,25 @@ export interface UninstallClientStateDeps {
|
|
|
18
22
|
remove: () => ConfigRemovalResult;
|
|
19
23
|
/** True while a timed-out icacls child still owns a path at or below the config directory. */
|
|
20
24
|
aclReapPending: (rootPath: string) => boolean;
|
|
25
|
+
/** Release the picker profile, then untrust the current and any pending predecessor picker root. */
|
|
26
|
+
removePickerArtifacts: () => Promise<PickerUninstallCleanup>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The picker root lives in the OS trust store (login keychain, CurrentUser Root on Windows), outside
|
|
31
|
+
* the config directory. Deleting config first would erase ca.pem and the pending-untrust journal,
|
|
32
|
+
* the only records naming which root to remove, and strand a trusted root after uninstall.
|
|
33
|
+
*/
|
|
34
|
+
async function removePickerArtifactsBeforeUninstall(): Promise<PickerUninstallCleanup> {
|
|
35
|
+
if (!supportsDesktopPicker()) return { ok: true };
|
|
36
|
+
const configDir = getConfigDir();
|
|
37
|
+
const [{ removeDesktopPickerArtifacts }, { drainPendingPickerCaUntrust }] = await Promise.all([
|
|
38
|
+
import("../claude/desktop-picker"),
|
|
39
|
+
import("../claude/intercept/picker-ca-cleanup"),
|
|
40
|
+
]);
|
|
41
|
+
const removed = await removeDesktopPickerArtifacts({ configDir });
|
|
42
|
+
if (!removed.ok) return removed;
|
|
43
|
+
return await drainPendingPickerCaUntrust(configDir) ? { ok: true } : { ok: false, residual: ["pending_trust"] };
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
const defaults: UninstallClientStateDeps = {
|
|
@@ -29,6 +52,7 @@ const defaults: UninstallClientStateDeps = {
|
|
|
29
52
|
cleanupIntegrations: cleanupOwnedIntegrationsBeforeUninstall,
|
|
30
53
|
remove: () => removeOwnedConfigState(getConfigDir()),
|
|
31
54
|
aclReapPending: rootPath => windowsSecretAclReapPendingAtOrBelow(rootPath),
|
|
55
|
+
removePickerArtifacts: removePickerArtifactsBeforeUninstall,
|
|
32
56
|
};
|
|
33
57
|
|
|
34
58
|
/** Restore connection-owned client artifacts before removing their ownership/recovery records. */
|
|
@@ -67,6 +91,14 @@ export async function removeOwnedConfigAfterDesktopCleanup(
|
|
|
67
91
|
throw new Error("Client cleanup refused: finish Desktop recovery before uninstalling.");
|
|
68
92
|
}
|
|
69
93
|
|
|
94
|
+
// Picker cleanup takes L itself, so it runs before the final lock. A residual keeps every
|
|
95
|
+
// record the retry needs: the config directory still names the root to untrust.
|
|
96
|
+
const picker = await deps.removePickerArtifacts();
|
|
97
|
+
if (!picker.ok) {
|
|
98
|
+
throw new Error(`Client cleanup refused: Claude Desktop picker cleanup left ${picker.residual?.join(", ") || "state"} behind; `
|
|
99
|
+
+ "approve the certificate removal prompt or run 'ocx claude desktop picker off', then rerun 'ocx uninstall'.");
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
// A connection can appear while asynchronous cleanup is finishing. The final
|
|
71
103
|
// inspection and actual remover share L, but never hold C while deleting its DB.
|
|
72
104
|
return deps.withLifecycle(async held => {
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare a bounded usage range with the previous equal window.
|
|
3
|
+
*
|
|
4
|
+
* Two sequential GET /api/usage reads with acknowledged custom bounds.
|
|
5
|
+
* `--range all` is refused: there is no previous equal window for open history.
|
|
6
|
+
*/
|
|
7
|
+
import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
|
|
8
|
+
import { fetchHubUsage } from "../client/hub-client";
|
|
9
|
+
import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
10
|
+
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
11
|
+
import { redactSecretString } from "../lib/redact";
|
|
12
|
+
import { rangeWindow, USAGE_SURFACES } from "../usage/summary";
|
|
13
|
+
import type { UsageTimeWindow } from "../usage/time-range";
|
|
14
|
+
import {
|
|
15
|
+
CliUsageError,
|
|
16
|
+
printData,
|
|
17
|
+
rejectArgs,
|
|
18
|
+
RuntimeApiError,
|
|
19
|
+
runtimeRequest,
|
|
20
|
+
runtimeBaseUrl,
|
|
21
|
+
takeFlag,
|
|
22
|
+
takeOption,
|
|
23
|
+
takeOptionWithSyntax,
|
|
24
|
+
type RuntimeApiDeps,
|
|
25
|
+
} from "./runtime-api";
|
|
26
|
+
|
|
27
|
+
export const USAGE_COMPARE_USAGE = `Usage:
|
|
28
|
+
ocx usage compare [--range <today|1d|7d|30d>] [--surface <all|codex|claude|grok>]
|
|
29
|
+
[--provider <name>] [--model <id>] [--api-key-id <id>] [--json]`;
|
|
30
|
+
|
|
31
|
+
export type CompareRange = "today" | "7d" | "30d";
|
|
32
|
+
export type UsageCompareDeps = RuntimeApiDeps & { now?: () => number };
|
|
33
|
+
|
|
34
|
+
const METRICS = ["requests", "totalTokens", "inputTokens", "outputTokens", "cachedInputTokens", "estimatedCostUsd"] as const;
|
|
35
|
+
type Metric = typeof METRICS[number];
|
|
36
|
+
type Totals = Partial<Record<Metric, number>>;
|
|
37
|
+
type ScopedUsage = {
|
|
38
|
+
customWindow?: boolean;
|
|
39
|
+
since?: number | null;
|
|
40
|
+
until?: number;
|
|
41
|
+
summary?: Totals;
|
|
42
|
+
usageIncomplete?: boolean;
|
|
43
|
+
filter?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function query(params: Record<string, string | number | undefined>): string {
|
|
47
|
+
const search = new URLSearchParams();
|
|
48
|
+
for (const [key, value] of Object.entries(params)) {
|
|
49
|
+
if (value !== undefined) search.set(key, String(value));
|
|
50
|
+
}
|
|
51
|
+
const encoded = search.toString();
|
|
52
|
+
return encoded ? `?${encoded}` : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseCompareRange(raw: string): CompareRange {
|
|
56
|
+
if (raw === "all") throw new CliUsageError("--range all has no previous equal window; use today, 1d, 7d or 30d", USAGE_COMPARE_USAGE);
|
|
57
|
+
if (raw === "today" || raw === "1d") return "today";
|
|
58
|
+
if (raw === "7d" || raw === "30d") return raw;
|
|
59
|
+
throw new CliUsageError("--range must be one of today, 1d, 7d, 30d", USAGE_COMPARE_USAGE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Named range at `now`, then the same range evaluated one equal window earlier. */
|
|
63
|
+
export function compareUsageWindows(range: CompareRange, now: number): {
|
|
64
|
+
current: UsageTimeWindow;
|
|
65
|
+
previous: UsageTimeWindow;
|
|
66
|
+
days: number;
|
|
67
|
+
} {
|
|
68
|
+
const days = range === "today" ? 1 : range === "7d" ? 7 : 30;
|
|
69
|
+
const { since } = rangeWindow(range, now);
|
|
70
|
+
if (since === null) throw new Error("compare requires a bounded usage range");
|
|
71
|
+
const prior = new Date(now);
|
|
72
|
+
prior.setDate(prior.getDate() - days);
|
|
73
|
+
const priorNow = prior.getTime();
|
|
74
|
+
const { since: previousSince } = rangeWindow(range, priorNow);
|
|
75
|
+
if (previousSince === null) throw new Error("compare requires a bounded usage range");
|
|
76
|
+
return {
|
|
77
|
+
current: Object.freeze({ since, until: now }),
|
|
78
|
+
previous: Object.freeze({ since: previousSince, until: priorNow }),
|
|
79
|
+
days,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function metric(value: unknown): number | undefined {
|
|
84
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function totalsOf(report: ScopedUsage): Totals {
|
|
88
|
+
const row = report.summary ?? {};
|
|
89
|
+
const totals: Totals = {};
|
|
90
|
+
for (const key of METRICS) {
|
|
91
|
+
const value = metric((row as Totals)[key]);
|
|
92
|
+
if (value !== undefined) totals[key] = value;
|
|
93
|
+
}
|
|
94
|
+
return totals;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function delta(current?: number, previous?: number): number | undefined {
|
|
98
|
+
if (current === undefined || previous === undefined) return undefined;
|
|
99
|
+
return current - previous;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function changePct(current?: number, previous?: number): number | null | undefined {
|
|
103
|
+
if (current === undefined || previous === undefined) return undefined;
|
|
104
|
+
if (previous === 0) return current === 0 ? 0 : null;
|
|
105
|
+
return (current - previous) / previous * 100;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function count(value: number | undefined): string {
|
|
109
|
+
return value === undefined ? "—" : value.toLocaleString("en-US");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function usd(value: number | undefined): string {
|
|
113
|
+
return value === undefined ? "—" : `~$${value.toFixed(4)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function signed(value: number | undefined, money: boolean): string {
|
|
117
|
+
if (value === undefined) return "—";
|
|
118
|
+
const abs = money ? usd(Math.abs(value)) : count(Math.abs(value));
|
|
119
|
+
if (value === 0) return money ? usd(0) : "0";
|
|
120
|
+
return `${value > 0 ? "+" : "-"}${abs}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pct(value: number | null | undefined): string {
|
|
124
|
+
if (value === undefined) return "—";
|
|
125
|
+
if (value === null) return "n/a";
|
|
126
|
+
const abs = Math.abs(value).toFixed(1);
|
|
127
|
+
if (value === 0) return "0.0%";
|
|
128
|
+
return `${value > 0 ? "+" : "-"}${abs}%`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function iso(ms: number): string {
|
|
132
|
+
return new Date(ms).toISOString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatCompare(input: {
|
|
136
|
+
range: CompareRange;
|
|
137
|
+
days: number;
|
|
138
|
+
currentWindow: UsageTimeWindow;
|
|
139
|
+
previousWindow: UsageTimeWindow;
|
|
140
|
+
current: Totals;
|
|
141
|
+
previous: Totals;
|
|
142
|
+
incomplete: boolean;
|
|
143
|
+
}): string[] {
|
|
144
|
+
const lines = [
|
|
145
|
+
`Usage compare — ${input.range} vs previous ${input.days} local day${input.days === 1 ? "" : "s"} (equal window)`,
|
|
146
|
+
`Current ${iso(input.currentWindow.since)} to ${iso(input.currentWindow.until)}`,
|
|
147
|
+
`Previous ${iso(input.previousWindow.since)} to ${iso(input.previousWindow.until)}`,
|
|
148
|
+
"",
|
|
149
|
+
];
|
|
150
|
+
if (input.incomplete) {
|
|
151
|
+
lines.push("WARNING: Usage is incomplete; some records could not be included. Totals reflect readable records only.", "");
|
|
152
|
+
}
|
|
153
|
+
const labels: Record<Metric, string> = {
|
|
154
|
+
requests: "Requests",
|
|
155
|
+
totalTokens: "Tokens",
|
|
156
|
+
inputTokens: "Input",
|
|
157
|
+
outputTokens: "Output",
|
|
158
|
+
cachedInputTokens: "Cached",
|
|
159
|
+
estimatedCostUsd: "Est. cost",
|
|
160
|
+
};
|
|
161
|
+
const rows = METRICS.map(key => {
|
|
162
|
+
const money = key === "estimatedCostUsd";
|
|
163
|
+
const show = money ? usd : count;
|
|
164
|
+
return [
|
|
165
|
+
labels[key],
|
|
166
|
+
show(input.current[key]),
|
|
167
|
+
show(input.previous[key]),
|
|
168
|
+
signed(delta(input.current[key], input.previous[key]), money),
|
|
169
|
+
pct(changePct(input.current[key], input.previous[key])),
|
|
170
|
+
];
|
|
171
|
+
});
|
|
172
|
+
const header = ["METRIC", "CURRENT", "PREVIOUS", "DELTA", "CHANGE"];
|
|
173
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map(row => row[i]!.length)));
|
|
174
|
+
const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
|
|
175
|
+
lines.push(line(header), ...rows.map(line), "", "Not a billing receipt. Subscription usage or provider credits may apply instead.");
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Private command-local authority; neither credentials nor fingerprints enter output.
|
|
180
|
+
function captureUsageOwner() {
|
|
181
|
+
const connection = readClientConnectionState();
|
|
182
|
+
if (connection.kind === "invalid" || connection.kind === "mismatched") {
|
|
183
|
+
throw new Error(`Client usage unavailable: ${connection.reason}`);
|
|
184
|
+
}
|
|
185
|
+
const token = readServiceApiTokenState();
|
|
186
|
+
if (connection.kind === "connected" && (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint)) {
|
|
187
|
+
throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
|
|
188
|
+
}
|
|
189
|
+
const headers = runningProxyUpdateHeaders();
|
|
190
|
+
const fingerprint = connection.kind === "connected" ? connection.value.tokenFingerprint
|
|
191
|
+
: serviceApiTokenFingerprint(headers.get("X-OpenCodex-API-Key") ?? "");
|
|
192
|
+
return { connection, token, headers, fingerprint };
|
|
193
|
+
}
|
|
194
|
+
type UsageOwner = ReturnType<typeof captureUsageOwner>;
|
|
195
|
+
|
|
196
|
+
function assertUsageOwner(owner: UsageOwner): void {
|
|
197
|
+
const current = readClientConnectionState();
|
|
198
|
+
const expected = owner.connection;
|
|
199
|
+
let same = current.kind === expected.kind;
|
|
200
|
+
if (expected.kind === "connected") {
|
|
201
|
+
const token = readServiceApiTokenState();
|
|
202
|
+
same = current.kind === "connected" && sameClientConnectionOwner(current.value, expected.value)
|
|
203
|
+
&& current.value.transport === expected.value.transport
|
|
204
|
+
&& JSON.stringify(current.value.link) === JSON.stringify(expected.value.link)
|
|
205
|
+
&& current.value.tokenFingerprint === owner.fingerprint
|
|
206
|
+
&& token.kind === "present" && token.fingerprint === owner.fingerprint;
|
|
207
|
+
} else {
|
|
208
|
+
same &&= serviceApiTokenFingerprint(runningProxyUpdateHeaders().get("X-OpenCodex-API-Key") ?? "") === owner.fingerprint;
|
|
209
|
+
}
|
|
210
|
+
if (!same) throw new Error("Usage owner changed during comparison; no comparison was produced. Retry for the current connection and credential.");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function readUsage(
|
|
214
|
+
suffix: string,
|
|
215
|
+
window: UsageTimeWindow,
|
|
216
|
+
apiKeyId: string | undefined,
|
|
217
|
+
deps: RuntimeApiDeps,
|
|
218
|
+
owner: UsageOwner,
|
|
219
|
+
): Promise<ScopedUsage> {
|
|
220
|
+
assertUsageOwner(owner);
|
|
221
|
+
const { connection, token } = owner;
|
|
222
|
+
let result: ScopedUsage;
|
|
223
|
+
if (connection.kind === "connected") {
|
|
224
|
+
if (apiKeyId !== undefined) {
|
|
225
|
+
throw new CliUsageError("--api-key-id is unavailable on connected clients; run on the hub for a selected key, or omit it for this client's usage", USAGE_COMPARE_USAGE);
|
|
226
|
+
}
|
|
227
|
+
if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
|
|
228
|
+
throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
|
|
229
|
+
}
|
|
230
|
+
const link = connection.value.transport === "link" && connection.value.link;
|
|
231
|
+
const peerFetch = deps.fetchImpl
|
|
232
|
+
?? (link ? (await import("../client/link-home")).linkBoundFetch(connection.value, token.token) : undefined);
|
|
233
|
+
result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix.replace(/^\?/, "")), {
|
|
234
|
+
fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
|
|
235
|
+
});
|
|
236
|
+
} else if (apiKeyId === undefined) {
|
|
237
|
+
result = await runtimeRequest(`/api/usage${suffix}`, { headers: owner.headers }, deps);
|
|
238
|
+
} else {
|
|
239
|
+
try {
|
|
240
|
+
result = await runtimeRequest(`/api/usage${suffix}`, { redirect: "error", credentials: "omit", headers: owner.headers }, deps);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
|
|
243
|
+
throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
|
|
244
|
+
}
|
|
245
|
+
if (result.filter?.apiKeyId !== apiKeyId) {
|
|
246
|
+
throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (result?.customWindow !== true || result.since !== window.since || result.until !== window.until) {
|
|
250
|
+
throw new Error("The server did not confirm the requested custom usage window. Upgrade and restart the proxy, then retry.");
|
|
251
|
+
}
|
|
252
|
+
assertUsageOwner(owner);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Compare the current named range with the previous equal window through two /api/usage reads. */
|
|
257
|
+
export async function handleUsageCompareCommand(argv: string[], deps: UsageCompareDeps = {}): Promise<void> {
|
|
258
|
+
const args = [...argv];
|
|
259
|
+
const wantsJson = takeFlag(args, "--json");
|
|
260
|
+
const range = parseCompareRange(takeOption(args, "--range") ?? "30d");
|
|
261
|
+
const surface = takeOption(args, "--surface") ?? "all";
|
|
262
|
+
const provider = takeOption(args, "--provider");
|
|
263
|
+
const model = takeOption(args, "--model");
|
|
264
|
+
const rawKeyId = takeOptionWithSyntax(args, "--api-key-id")?.value;
|
|
265
|
+
const apiKeyId = rawKeyId?.trim();
|
|
266
|
+
if (apiKeyId !== undefined && !apiKeyId) throw new CliUsageError("--api-key-id must not be blank", USAGE_COMPARE_USAGE);
|
|
267
|
+
if (takeOption(args, "--since") !== undefined || takeOption(args, "--until") !== undefined) {
|
|
268
|
+
throw new CliUsageError("compare derives both windows; do not pass --since or --until", USAGE_COMPARE_USAGE);
|
|
269
|
+
}
|
|
270
|
+
if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
|
|
271
|
+
throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE_COMPARE_USAGE);
|
|
272
|
+
}
|
|
273
|
+
rejectArgs(args.map(redactSecretString), USAGE_COMPARE_USAGE);
|
|
274
|
+
const now = deps.now?.() ?? Date.now();
|
|
275
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new CliUsageError("compare clock is invalid", USAGE_COMPARE_USAGE);
|
|
276
|
+
const windows = compareUsageWindows(range, now);
|
|
277
|
+
const filters = { range, surface, provider, model, apiKeyId };
|
|
278
|
+
const owner = captureUsageOwner();
|
|
279
|
+
const readDeps = owner.connection.kind === "disconnected" ? { ...deps, baseUrl: await runtimeBaseUrl(deps) } : deps;
|
|
280
|
+
const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, readDeps, owner);
|
|
281
|
+
const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, readDeps, owner);
|
|
282
|
+
const current = totalsOf(currentReport);
|
|
283
|
+
const previous = totalsOf(previousReport);
|
|
284
|
+
const deltaTotals: Totals = {};
|
|
285
|
+
const change: Partial<Record<Metric, number | null>> = {};
|
|
286
|
+
for (const key of METRICS) {
|
|
287
|
+
const d = delta(current[key], previous[key]);
|
|
288
|
+
if (d !== undefined) deltaTotals[key] = d;
|
|
289
|
+
const pctValue = changePct(current[key], previous[key]);
|
|
290
|
+
if (pctValue !== undefined) change[key] = pctValue;
|
|
291
|
+
}
|
|
292
|
+
const incomplete = currentReport.usageIncomplete === true || previousReport.usageIncomplete === true;
|
|
293
|
+
const result = {
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
range,
|
|
296
|
+
surface,
|
|
297
|
+
days: windows.days,
|
|
298
|
+
filter: { provider: provider ?? null, model: model ?? null, apiKeyId: apiKeyId ?? null },
|
|
299
|
+
current: { since: windows.current.since, until: windows.current.until, summary: current, usageIncomplete: currentReport.usageIncomplete === true },
|
|
300
|
+
previous: { since: windows.previous.since, until: windows.previous.until, summary: previous, usageIncomplete: previousReport.usageIncomplete === true },
|
|
301
|
+
delta: deltaTotals,
|
|
302
|
+
change,
|
|
303
|
+
incomplete,
|
|
304
|
+
};
|
|
305
|
+
printData(result, wantsJson, wantsJson ? undefined : formatCompare({
|
|
306
|
+
range, days: windows.days, currentWindow: windows.current, previousWindow: windows.previous,
|
|
307
|
+
current, previous, incomplete,
|
|
308
|
+
}));
|
|
309
|
+
}
|