@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** A management route a capability drives. Path text only; never a handler reference. */
|
|
2
|
+
export interface CapabilityRoute {
|
|
3
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CapabilityFlag {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly value?: "string" | "number" | "boolean";
|
|
10
|
+
readonly required?: boolean;
|
|
11
|
+
readonly summary: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How a capability emits JSON.
|
|
16
|
+
*
|
|
17
|
+
* - `payload`: the API payload, largely unwrapped.
|
|
18
|
+
* - `envelope`: a CLI-shaped object with its own schema.
|
|
19
|
+
* - `none`: no `--json` mode.
|
|
20
|
+
*/
|
|
21
|
+
export type CapabilityJsonMode = "payload" | "envelope" | "none";
|
|
22
|
+
|
|
23
|
+
export interface Capability {
|
|
24
|
+
/** Command path, e.g. `["account", "pause"]`. */
|
|
25
|
+
readonly command: readonly string[];
|
|
26
|
+
readonly summary: string;
|
|
27
|
+
readonly routes: readonly CapabilityRoute[];
|
|
28
|
+
readonly flags: readonly CapabilityFlag[];
|
|
29
|
+
readonly mutates: boolean;
|
|
30
|
+
readonly json: CapabilityJsonMode;
|
|
31
|
+
/** Complete CLI usage, including operands, when verified for this leaf. */
|
|
32
|
+
readonly usage?: string;
|
|
33
|
+
readonly details?: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Extra banner rows this capability owns, for surfaces the banner shows separately
|
|
36
|
+
* from the bare command (`ocx restore back`, `ocx doctor --reclaim-response-temps`).
|
|
37
|
+
* Without this the banner cannot equal the capability set: it legitimately carries more
|
|
38
|
+
* rows than there are commands.
|
|
39
|
+
*/
|
|
40
|
+
readonly bannerLines?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Surfaces resolved in the CLI head, before dispatch.
|
|
45
|
+
*
|
|
46
|
+
* They belong in `ocx capabilities` output and in the banner, but not in `CLI_COMMANDS`:
|
|
47
|
+
* `--version`, `-v`, and `version` are answered at `root.ts` and exit, so none of them is
|
|
48
|
+
* a runner key to parity-check against.
|
|
49
|
+
*/
|
|
50
|
+
export interface HeadCapability {
|
|
51
|
+
readonly invocations: readonly string[];
|
|
52
|
+
readonly summary: string;
|
|
53
|
+
readonly bannerLine: string;
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
2
|
+
import { CliUsageError, RuntimeApiError, PROXY_NOT_RUNNING_MESSAGE, printData, terminalSafeText } from "./runtime-api";
|
|
3
|
+
|
|
4
|
+
function own(value: unknown, key: string): unknown {
|
|
5
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
6
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
7
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A small error/exit boundary; routes and domain DTO validation belong to callers. */
|
|
11
|
+
export async function runCatalogAction(
|
|
12
|
+
action: () => Promise<number | void>, knownErrors: Readonly<Record<string, string>> = {},
|
|
13
|
+
): Promise<number> {
|
|
14
|
+
try {
|
|
15
|
+
return (await action()) ?? 0;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof CliUsageError) {
|
|
18
|
+
// New domain parsers use static messages instead of raw JSON/validator exceptions.
|
|
19
|
+
console.error(`Error: ${terminalSafeText(error.message)}`);
|
|
20
|
+
if (error.usage) for (const line of error.usage.split("\n")) console.error(terminalSafeText(line));
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
if (error instanceof RuntimeApiError) {
|
|
24
|
+
if (error.code) {
|
|
25
|
+
console.error(`Error: ${error.code === "proxy_not_running" ? PROXY_NOT_RUNNING_MESSAGE
|
|
26
|
+
: "This listener has no management API. Run management commands on the connected Hub. No request was sent."}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
|
|
30
|
+
const known = typeof code === "string" && Object.hasOwn(knownErrors, code) ? knownErrors[code] : undefined;
|
|
31
|
+
const message = error.status === 404 ? "The requested record or operation was not found on the selected target."
|
|
32
|
+
: error.status === 409 ? "The selected target refused a conflicting change. Read back before trying again."
|
|
33
|
+
: error.status === 503 ? "Management API is unavailable. Use the intended running proxy; connected clients must make management changes on their Hub. A write outcome may be unknown."
|
|
34
|
+
: "The management request failed. Inspect the selected target before retrying; the write outcome may be unknown.";
|
|
35
|
+
console.error(`Error: ${terminalSafeText(known ?? message)}`);
|
|
36
|
+
return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
|
|
37
|
+
}
|
|
38
|
+
console.error("Error: Management operation did not return a usable outcome. Inspect the selected target before retrying; no rollback is implied.");
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Caller supplies a validated, rebuilt public DTO, not an arbitrary server body.
|
|
45
|
+
* This helper normalizes only the shared catalog disposition and preserves its exit.
|
|
46
|
+
*/
|
|
47
|
+
export function printCatalogResult(
|
|
48
|
+
data: Record<string, unknown>, refresh: unknown, wantsJson: boolean, lines: readonly string[],
|
|
49
|
+
): number {
|
|
50
|
+
const catalogRefresh = normalizeCatalogDisposition(refresh);
|
|
51
|
+
if (catalogRefresh === null) throw new Error("Invalid catalog outcome");
|
|
52
|
+
const converged = catalogRefresh.status === "committed" && !catalogRefresh.degraded;
|
|
53
|
+
const outcome = converged ? "Catalog committed."
|
|
54
|
+
: "Change saved, but catalog convergence is incomplete. Read back before retrying.";
|
|
55
|
+
printData({ ...data, catalogRefresh }, wantsJson, [...lines, outcome]);
|
|
56
|
+
return converged ? 0 : 1;
|
|
57
|
+
}
|
|
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { loadConfig } from "../config";
|
|
5
|
+
import { readConfigFileSnapshot } from "../config/diagnostics";
|
|
6
|
+
import { chatgptDesktopConfigIssue } from "../config/schema/chatgpt-desktop";
|
|
5
7
|
import {
|
|
6
8
|
chatgptShimLauncherPath,
|
|
7
9
|
resolveChatgptCodexBinary,
|
|
@@ -21,6 +23,20 @@ function run(command: string, args: string[]) {
|
|
|
21
23
|
return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Why the config file's `chatgptDesktop` block reads as absent, or null when it is valid, absent
|
|
28
|
+
* or the file cannot be parsed (the config loader reports that case itself).
|
|
29
|
+
*/
|
|
30
|
+
function chatgptDesktopIssueInFile(): string | null {
|
|
31
|
+
const { raw } = readConfigFileSnapshot();
|
|
32
|
+
if (raw === undefined) return null;
|
|
33
|
+
try {
|
|
34
|
+
return chatgptDesktopConfigIssue(JSON.parse(raw.replace(/^\uFEFF/, "")));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/**
|
|
25
41
|
* The installed app, found and confirmed by bundle identifier (com.openai.codex) the same way the
|
|
26
42
|
* desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
|
|
@@ -90,7 +106,10 @@ export async function handleChatgptCommand(args: string[], platform: NodeJS.Plat
|
|
|
90
106
|
const install = discoverApp();
|
|
91
107
|
if (sub === "status") {
|
|
92
108
|
const app = install ? appState(install, launcher) : { running: false, shim: false };
|
|
93
|
-
|
|
109
|
+
const configIssue = config.chatgptDesktop?.appServerShim === true ? null : chatgptDesktopIssueInFile();
|
|
110
|
+
const flag = config.chatgptDesktop?.appServerShim === true ? "on"
|
|
111
|
+
: configIssue ? `off (config.json ${configIssue}; the whole chatgptDesktop block is ignored)` : "off";
|
|
112
|
+
console.log(`app-server shim (experimental): ${flag}
|
|
94
113
|
launcher: ${existsSync(launcher) ? "present" : "absent"}
|
|
95
114
|
app: ${install ? (app.running ? "running" : "not running") : "not installed"}
|
|
96
115
|
CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
|
|
@@ -101,23 +120,30 @@ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
|
|
|
101
120
|
console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
|
|
102
121
|
return 1;
|
|
103
122
|
}
|
|
123
|
+
let binary: string | undefined;
|
|
104
124
|
if (sub === "launch") {
|
|
105
125
|
if (config.chatgptDesktop?.appServerShim !== true) {
|
|
106
|
-
|
|
126
|
+
// Read the file only to explain a refusal; restore and a valid launch never touch it.
|
|
127
|
+
const configIssue = chatgptDesktopIssueInFile();
|
|
128
|
+
console.error(configIssue
|
|
129
|
+
? `Experimental shim disabled: config.json ${configIssue}. The whole chatgptDesktop block is ignored until that is fixed.`
|
|
130
|
+
: "Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.");
|
|
107
131
|
return 1;
|
|
108
132
|
}
|
|
109
|
-
|
|
133
|
+
binary = resolveChatgptCodexBinary(install.root) ?? undefined;
|
|
110
134
|
if (!binary) {
|
|
111
135
|
console.error(`No bundled app-server binary was found in ${install.root}; the shim cannot launch this build.`);
|
|
112
136
|
return 1;
|
|
113
137
|
}
|
|
114
|
-
const untrusted = untrustedChatgptBundleReason(install.root, binary);
|
|
115
|
-
if (untrusted) {
|
|
116
|
-
console.error(`Refusing to launch the shim: ${untrusted}.`);
|
|
117
|
-
return 1;
|
|
118
|
-
}
|
|
119
|
-
writeChatgptShimLauncher(undefined, binary);
|
|
120
138
|
}
|
|
139
|
+
// Both relaunch paths execute the discovered bundle. Restore validates the app
|
|
140
|
+
// shell without requiring an app-server binary or the experimental opt-in flag.
|
|
141
|
+
const untrusted = untrustedChatgptBundleReason(install.root, binary);
|
|
142
|
+
if (untrusted) {
|
|
143
|
+
console.error(`Refusing to ${sub === "launch" ? "launch the shim" : "restore ChatGPT"}: ${untrusted}.`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (binary) writeChatgptShimLauncher(undefined, binary);
|
|
121
147
|
if (!(await quitApp(install, launcher))) {
|
|
122
148
|
console.error("ChatGPT did not quit; quit it manually and retry.");
|
|
123
149
|
return 1;
|
|
@@ -64,6 +64,23 @@ export async function reconcileClientStartupBeforeReady<T>(
|
|
|
64
64
|
return result;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Establish catalog observation before any unrelated startup reconciliation awaits. */
|
|
68
|
+
export async function syncCodexBeforeCatalogObservation<T>(
|
|
69
|
+
gate: ReadinessGate,
|
|
70
|
+
syncCodex: (gate: ReadinessGate) => Promise<T>,
|
|
71
|
+
startObservation: () => void,
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
let ready = false;
|
|
74
|
+
const forwardingGate: ReadinessGate = {
|
|
75
|
+
getStatus: () => gate.getStatus(),
|
|
76
|
+
markReady: () => { ready = true; gate.markReady(); },
|
|
77
|
+
markFailed: () => { ready = false; gate.markFailed(); },
|
|
78
|
+
};
|
|
79
|
+
const result = await syncCodex(forwardingGate);
|
|
80
|
+
if (ready) startObservation();
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Reconcile the generated Claude Code roster after the proxy listener is live.
|
|
69
86
|
*
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Runtime-owned Desktop profile editing. Saving never applies a native configuration. */
|
|
2
|
+
import { DESKTOP_FAMILIES, parseDesktopProfile, type DesktopProfile } from "../claude/desktop-profile";
|
|
3
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
4
|
+
import { readJsonInput, serializeManagementJson } from "./json-input";
|
|
5
|
+
import { CliUsageError, printData, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
|
|
6
|
+
|
|
7
|
+
export interface DesktopProfileCliDeps extends RuntimeApiDeps {
|
|
8
|
+
runtimeRequestImpl?: typeof runtimeRequest;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const USAGE = "Usage: ocx claude desktop profile show [--json]\n ocx claude desktop profile import FILE|- [--json]";
|
|
12
|
+
|
|
13
|
+
function record(value: unknown): Record<string, unknown> {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Desktop state");
|
|
15
|
+
return value as Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function text(value: unknown): string {
|
|
19
|
+
if (typeof value !== "string" || !value.length) throw new Error("Invalid Desktop text");
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function boolean(value: unknown): boolean {
|
|
24
|
+
if (typeof value !== "boolean") throw new Error("Invalid Desktop flag");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function modelIdentity(row: Record<string, unknown>) {
|
|
29
|
+
const route = text(row.route);
|
|
30
|
+
if (!route.includes("/")) throw new Error("Invalid Desktop route");
|
|
31
|
+
if (row.contextWindow !== undefined && (typeof row.contextWindow !== "number"
|
|
32
|
+
|| !Number.isSafeInteger(row.contextWindow) || row.contextWindow <= 0)) throw new Error("Invalid Desktop context window");
|
|
33
|
+
return { route, label: text(row.label), ...(row.contextWindow === undefined ? {} : { contextWindow: row.contextWindow as number }) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function desktopState(raw: unknown, saved: boolean) {
|
|
37
|
+
const value = record(raw);
|
|
38
|
+
if (saved && value.ok !== true) throw new Error("Desktop save was not confirmed");
|
|
39
|
+
const profile = parseDesktopProfile(value.profile);
|
|
40
|
+
if (!Array.isArray(value.models) || !Array.isArray(value.rendered)
|
|
41
|
+
|| !Number.isSafeInteger(value.port) || (value.port as number) < 1 || (value.port as number) > 65535) {
|
|
42
|
+
throw new Error("Invalid Desktop state");
|
|
43
|
+
}
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
const models = value.models.map(rawModel => {
|
|
46
|
+
const row = record(rawModel), identity = modelIdentity(row), assignment = record(row.assignment);
|
|
47
|
+
const expected = profile.assignments[identity.route];
|
|
48
|
+
if (!expected || seen.has(identity.route) || assignment.family !== expected.family || assignment.alias !== expected.alias) {
|
|
49
|
+
throw new Error("Invalid Desktop assignment");
|
|
50
|
+
}
|
|
51
|
+
seen.add(identity.route);
|
|
52
|
+
return { ...identity, available: boolean(row.available), effortSupported: boolean(row.effortSupported),
|
|
53
|
+
supports1m: boolean(row.supports1m), assignment: { family: expected.family, alias: expected.alias } };
|
|
54
|
+
});
|
|
55
|
+
if (seen.size !== Object.keys(profile.assignments).length) throw new Error("Incomplete Desktop models");
|
|
56
|
+
const renderedSeen = new Set<string>();
|
|
57
|
+
const rendered = value.rendered.map(rawModel => {
|
|
58
|
+
const row = record(rawModel), identity = modelIdentity(row);
|
|
59
|
+
if (!DESKTOP_FAMILIES.includes(row.family as typeof DESKTOP_FAMILIES[number])
|
|
60
|
+
|| profile.assignments[identity.route]?.family !== row.family || renderedSeen.has(identity.route)
|
|
61
|
+
|| !models.some(model => model.route === identity.route && model.available)) throw new Error("Invalid rendered Desktop model");
|
|
62
|
+
renderedSeen.add(identity.route);
|
|
63
|
+
return { ...identity, name: text(row.name), family: row.family as typeof DESKTOP_FAMILIES[number],
|
|
64
|
+
isFamilyDefault: boolean(row.isFamilyDefault), supports1m: boolean(row.supports1m) };
|
|
65
|
+
});
|
|
66
|
+
if (renderedSeen.size !== models.filter(model => model.available).length) throw new Error("Incomplete rendered Desktop models");
|
|
67
|
+
return { ...(saved ? { ok: true as const } : {}), profile, models, rendered, port: value.port as number };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Arguments start immediately after `claude desktop profile`. */
|
|
71
|
+
export async function handleClaudeDesktopProfileCommand(argv: string[], deps: DesktopProfileCliDeps = {}): Promise<number> {
|
|
72
|
+
return runCatalogAction(async () => {
|
|
73
|
+
const args = [...argv], wantsJson = takeFlag(args, "--json");
|
|
74
|
+
const action = args.shift();
|
|
75
|
+
if ((action !== "show" && action !== "import") || (action === "show" && args.length !== 0)
|
|
76
|
+
|| (action === "import" && (args.length !== 1 || !args[0]?.trim() || (args[0]!.startsWith("-") && args[0] !== "-")))) {
|
|
77
|
+
throw new CliUsageError("Desktop profile accepts show or import with an explicit file or '-'. Saving and applying are separate actions.", USAGE);
|
|
78
|
+
}
|
|
79
|
+
let body: string | undefined;
|
|
80
|
+
if (action === "import") {
|
|
81
|
+
const input = await readJsonInput(args[0]!, deps);
|
|
82
|
+
let profile: DesktopProfile;
|
|
83
|
+
try { profile = parseDesktopProfile(input); }
|
|
84
|
+
catch { throw new CliUsageError("Desktop profile input is invalid. Supply a version-1 profile with valid assignments and family defaults."); }
|
|
85
|
+
body = serializeManagementJson({ profile });
|
|
86
|
+
}
|
|
87
|
+
const raw = await (deps.runtimeRequestImpl ?? runtimeRequest)("/api/claude-desktop", {
|
|
88
|
+
method: action === "show" ? "GET" : "PUT", redirect: "error",
|
|
89
|
+
...(body === undefined ? {} : { headers: { "content-type": "application/json" }, body }),
|
|
90
|
+
}, deps);
|
|
91
|
+
const state = desktopState(raw, action === "import");
|
|
92
|
+
printData(state, wantsJson, [
|
|
93
|
+
action === "import" ? "Desktop profile saved on the selected proxy. It has not been applied by this command." : "Desktop profile on the selected proxy.",
|
|
94
|
+
`Models: ${state.models.length}; available: ${state.models.filter(model => model.available).length}; port: ${state.port}`,
|
|
95
|
+
...DESKTOP_FAMILIES.map(family => `${family}: ${state.profile.defaults[family] ?? "none"}`),
|
|
96
|
+
...(action === "import" ? ["Next: review with ocx claude desktop profile show; apply separately with ocx claude desktop apply."] : []),
|
|
97
|
+
]);
|
|
98
|
+
return 0;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -321,6 +321,27 @@ export function defaultDesktopApplyMode(
|
|
|
321
321
|
return connection.kind === "connected" ? "gateway" : "first-party";
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
const DESKTOP_APPLY_FLAGS = ["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"] as const;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A rejected argument is never echoed: an option-shaped token can carry a value (`-tVALUE`,
|
|
328
|
+
* `--opt=VALUE`, a dash-prefixed operand) and any token can carry terminal control characters, so
|
|
329
|
+
* the error counts them and lists the options that do exist instead.
|
|
330
|
+
*/
|
|
331
|
+
function unknownApplyArgsError(count: number): string {
|
|
332
|
+
return `알 수 없는 인자 ${count}개 (값은 표시하지 않습니다). 사용 가능한 옵션: ${DESKTOP_APPLY_FLAGS.join(" ")}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A rejected route is not echoed: whatever sits in that position may be a misplaced credential. */
|
|
336
|
+
const UNAVAILABLE_ROUTE = "현재 사용할 수 없는 모델입니다. 사용 가능한 모델은 ocx claude desktop show로 확인하세요.";
|
|
337
|
+
|
|
338
|
+
/** Profile files are user-chosen paths; a failure reports the operation and error code only. */
|
|
339
|
+
function profileFileError(operation: "export" | "import", error: unknown): Error {
|
|
340
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
341
|
+
const reason = typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : error instanceof SyntaxError ? "invalid JSON" : "failed";
|
|
342
|
+
return new Error(`Claude Desktop 프로필 ${operation === "export" ? "내보내기" : "가져오기"} 실패: ${reason}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
export function parseDesktopApplyArgs(
|
|
325
346
|
flags: string[],
|
|
326
347
|
config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
|
|
@@ -330,8 +351,8 @@ export function parseDesktopApplyArgs(
|
|
|
330
351
|
const wantsFirstParty = flags.includes("--first-party");
|
|
331
352
|
const wantsGateway = flags.includes("--gateway") || shapeFlags.length > 0;
|
|
332
353
|
if (wantsFirstParty && wantsGateway) return { error: "--first-party cannot be combined with --gateway or gateway shape flags." };
|
|
333
|
-
const unknown = flags.filter(arg => !
|
|
334
|
-
if (unknown.length > 0) return { error:
|
|
354
|
+
const unknown = flags.filter(arg => !(DESKTOP_APPLY_FLAGS as readonly string[]).includes(arg));
|
|
355
|
+
if (unknown.length > 0) return { error: unknownApplyArgsError(unknown.length) };
|
|
335
356
|
const kind: ClaudeDesktopMode = wantsFirstParty
|
|
336
357
|
? "first-party"
|
|
337
358
|
: wantsGateway ? "gateway" : defaultDesktopApplyMode(config, readClientConnectionState(), observed ?? observeClaudeDesktopMode(config));
|
|
@@ -679,6 +700,12 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
679
700
|
printDesktopHelp();
|
|
680
701
|
return 0;
|
|
681
702
|
}
|
|
703
|
+
if (command === "profile") {
|
|
704
|
+
const { handleClaudeDesktopProfileCommand } = await import("./claude-desktop-profile");
|
|
705
|
+
return handleClaudeDesktopProfileCommand(argv.slice(1), {
|
|
706
|
+
runtimeRequestImpl: deps.runtimeRequestImpl, findLiveProxy: deps.findLiveProxyImpl,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
682
709
|
|
|
683
710
|
// Mode flags remain apply aliases and are parsed before subcommands.
|
|
684
711
|
const applyFlags = argv.filter(arg => APPLY_FLAGS.includes(arg));
|
|
@@ -806,7 +833,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
806
833
|
if (command === "move") {
|
|
807
834
|
const [, route, familyRaw, ...flags] = argv;
|
|
808
835
|
if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new CliUsageError("Usage: ocx claude desktop move <route> <family> [--default]");
|
|
809
|
-
if (!state.models.some(model => model.route === route && model.available)) throw new Error(
|
|
836
|
+
if (!state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
|
|
810
837
|
const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
|
|
811
838
|
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
812
839
|
console.log(`${route} 모델을 ${familyRaw} 그룹으로 옮겼습니다.`);
|
|
@@ -816,7 +843,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
816
843
|
const [, familyRaw, routeRaw] = argv;
|
|
817
844
|
if (!isFamily(familyRaw) || !routeRaw || argv.length !== 3) throw new CliUsageError("Usage: ocx claude desktop default <family> <route|none>");
|
|
818
845
|
const route = routeRaw === "none" ? null : routeRaw;
|
|
819
|
-
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(
|
|
846
|
+
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
|
|
820
847
|
const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
|
|
821
848
|
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
822
849
|
console.log(`${familyRaw} 기본 모델을 ${route ?? "없음"}으로 지정했습니다.`);
|
|
@@ -827,14 +854,20 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
827
854
|
if (!target || argv.length !== 2) throw new CliUsageError("Usage: ocx claude desktop export <path|->");
|
|
828
855
|
const json = JSON.stringify(state.profile, null, 2) + "\n";
|
|
829
856
|
if (target === "-") process.stdout.write(json);
|
|
830
|
-
else
|
|
857
|
+
else {
|
|
858
|
+
try { writeFileSync(resolve(target), json, { encoding: "utf8", mode: 0o600 }); }
|
|
859
|
+
catch (error) { throw profileFileError("export", error); }
|
|
860
|
+
}
|
|
831
861
|
return 0;
|
|
832
862
|
}
|
|
833
863
|
if (command === "import") {
|
|
834
864
|
const source = argv[1];
|
|
835
865
|
const flags = argv.slice(2);
|
|
836
866
|
if (!source || flags.some(flag => flag !== "--apply")) throw new CliUsageError("Usage: ocx claude desktop import <path> [--apply]");
|
|
837
|
-
|
|
867
|
+
let raw: unknown;
|
|
868
|
+
try { raw = JSON.parse(readFileSync(resolve(source), "utf8")); }
|
|
869
|
+
catch (error) { throw profileFileError("import", error); }
|
|
870
|
+
const profile = parseDesktopProfile(raw);
|
|
838
871
|
const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
|
|
839
872
|
if (flags.includes("--apply")) assertNoClientDisconnectPending();
|
|
840
873
|
if (flags.includes("--apply") && readClientConnectionState().kind !== "disconnected") {
|
package/src/cli/claude.ts
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* `claude` CLI with stdio inherited. User-exported env wins except when a stale
|
|
8
8
|
* loopback opencodex base URL points at a different proxy port.
|
|
9
9
|
*/
|
|
10
|
+
import { resolveSubagentForceModel } from "../claude/subagent-model";
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
11
12
|
import { loadConfig } from "../config";
|
|
12
13
|
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
13
14
|
import { CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
|
|
14
15
|
import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
15
|
-
import { claudeConfigDir,
|
|
16
|
+
import { claudeConfigDir, fetchGatewayModels, writeGatewayModelCache } from "../claude/gateway-cache";
|
|
16
17
|
import { commandInvocation } from "../lib/win-exec";
|
|
17
18
|
import { isProxyAdmissionSecret } from "../server/auth-cors";
|
|
18
19
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
@@ -50,6 +51,8 @@ export interface ClaudeRoutingTarget {
|
|
|
50
51
|
* launch base so detection and the spawned process can never disagree (audit R3-3).
|
|
51
52
|
*/
|
|
52
53
|
export type ClaudeEnvDeps = {
|
|
54
|
+
forceAvailable?: readonly string[];
|
|
55
|
+
forceAvailableSelectors?: readonly string[];
|
|
53
56
|
authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
|
|
54
57
|
/** Test seam; production uses the authenticated Node-launcher context. */
|
|
55
58
|
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
|
|
@@ -213,6 +216,15 @@ export function buildClaudeEnv(
|
|
|
213
216
|
if (env[name] !== undefined && env[name] !== "") return; // user wins
|
|
214
217
|
env[name] = value;
|
|
215
218
|
};
|
|
219
|
+
if (config.claudeCode?.subagentModelForce !== undefined) {
|
|
220
|
+
const forced = resolveSubagentForceModel(config, contextWindows, deps.forceAvailableSelectors !== undefined ? { selectors: deps.forceAvailableSelectors } : { entries: deps.forceAvailable ?? [] });
|
|
221
|
+
if (forced) {
|
|
222
|
+
setDefault("CLAUDE_CODE_SUBAGENT_MODEL", forced);
|
|
223
|
+
setDefault("CLAUDE_CODE_SUBAGENT_MODEL_FORCE", "1");
|
|
224
|
+
} else {
|
|
225
|
+
(deps.warn ?? console.warn)("⚠ Claude subagent force target is invalid or unavailable; launch override skipped.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
216
228
|
if (deps.allowRootSkipPermissions === true) {
|
|
217
229
|
setDefault("IS_SANDBOX", "1");
|
|
218
230
|
}
|
|
@@ -420,6 +432,7 @@ export function buildClaudeEnv(
|
|
|
420
432
|
* decides to launch natively, so a wrong destination here silently downgrades every launch.
|
|
421
433
|
*/
|
|
422
434
|
export interface ClaudeCodeLiveState {
|
|
435
|
+
forceAvailable?: string[];
|
|
423
436
|
contextWindows: Record<string, number>;
|
|
424
437
|
enabled?: boolean;
|
|
425
438
|
}
|
|
@@ -434,9 +447,10 @@ export async function fetchClaudeCodeState(config: OcxConfig, port: number, time
|
|
|
434
447
|
signal: AbortSignal.timeout(timeoutMs),
|
|
435
448
|
});
|
|
436
449
|
if (!res.ok) return { contextWindows: {} };
|
|
437
|
-
const body = await res.json() as { contextWindows?: Record<string, number>; enabled?: boolean };
|
|
450
|
+
const body = await res.json() as { contextWindows?: Record<string, number>; enabled?: boolean; forceAvailable?: string[] };
|
|
438
451
|
return {
|
|
439
452
|
contextWindows: body.contextWindows && typeof body.contextWindows === "object" ? body.contextWindows : {},
|
|
453
|
+
...(Array.isArray(body.forceAvailable) ? { forceAvailable: body.forceAvailable.filter(value => typeof value === "string") } : {}),
|
|
440
454
|
...(typeof body.enabled === "boolean" ? { enabled: body.enabled } : {}),
|
|
441
455
|
};
|
|
442
456
|
} catch {
|
|
@@ -774,6 +788,7 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
774
788
|
if (preflight.kind === "native") return launchNativeClaude(config, args, preflight.notice);
|
|
775
789
|
let route: number | ClaudeRoutingTarget;
|
|
776
790
|
let contextWindows: Record<string, number>;
|
|
791
|
+
let forceAvailable: readonly string[] | undefined;
|
|
777
792
|
/** The hub's featured roster on a connected client; undefined means "use local config". */
|
|
778
793
|
let hubRoster: readonly string[] | undefined;
|
|
779
794
|
if (clientState.kind === "connected") {
|
|
@@ -792,18 +807,19 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
792
807
|
if (plan.kind === "native") return launchNativeClaude(config, args, plan.notice);
|
|
793
808
|
route = port;
|
|
794
809
|
contextWindows = liveState.contextWindows;
|
|
810
|
+
forceAvailable = liveState.forceAvailable ?? [];
|
|
795
811
|
}
|
|
812
|
+
const gateway = await fetchGatewayModels(route, { admissionConfig: config });
|
|
813
|
+
const forceAvailableSelectors = typeof route === "number" ? undefined : gateway?.models.map(model => model.id) ?? [];
|
|
796
814
|
const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
|
|
797
|
-
const env = buildClaudeEnv(config, route, process.env, contextWindows, { allowRootSkipPermissions });
|
|
815
|
+
const env = buildClaudeEnv(config, route, process.env, contextWindows, { allowRootSkipPermissions, forceAvailable, forceAvailableSelectors });
|
|
798
816
|
if (allowRootSkipPermissions) {
|
|
799
817
|
console.error(rootSkipPermissionsNotice(env));
|
|
800
818
|
}
|
|
801
819
|
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
|
|
802
820
|
// never refreshes it, so the picker would keep showing yesterday's aliases.
|
|
803
821
|
try {
|
|
804
|
-
const cachePath =
|
|
805
|
-
? await refreshGatewayModelCacheFromProxy(route, { admissionConfig: config })
|
|
806
|
-
: await refreshGatewayModelCacheFromProxy(route, { admissionConfig: config });
|
|
822
|
+
const cachePath = gateway ? writeGatewayModelCache(gateway.baseUrl, gateway.models) : null;
|
|
807
823
|
if (cachePath === null) {
|
|
808
824
|
console.error("⚠ Gateway model cache could not be refreshed; the model picker may be stale.");
|
|
809
825
|
}
|
|
@@ -17,6 +17,7 @@ const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
|
|
|
17
17
|
|
|
18
18
|
export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
|
|
19
19
|
if (command === "uninstall" || command === "remove") return true;
|
|
20
|
+
if (command === "status" || command === "doctor") return true;
|
|
20
21
|
// `lab` is read-only inspection; it must not trigger shim side effects.
|
|
21
22
|
if (command === "lab") return true;
|
|
22
23
|
// `resolve` is read-only inspection for embedding shells: a lookup made to populate
|
|
@@ -29,7 +30,7 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
|
|
|
29
30
|
// or future actions. A later `apply` implementation must own its preflight.
|
|
30
31
|
if (command === "system" && args[1] === "codex-cli-update") return true;
|
|
31
32
|
if (command === "__update-badge") return true;
|
|
32
|
-
return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
|
|
33
|
+
return command === "codex-shim" && ["install", "status", "uninstall", "remove"].includes(args[1] ?? "");
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function maybeAutoRestoreCodexShim(
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
getCodexRoutingKind,
|
|
4
4
|
type CodexRoutingKind,
|
|
5
5
|
} from "../codex/inject";
|
|
6
|
+
import { diagnoseCodexShim, type CodexShimDiagnostic } from "../codex/shim";
|
|
7
|
+
import { overlayActivationHint } from "../codex/shim-overlay";
|
|
8
|
+
import { findFirstCodexOnPath, type CodexPathCandidate } from "../codex/shim-path-resolution";
|
|
6
9
|
import { loadConfig, resolveEnvValue } from "../config";
|
|
7
10
|
|
|
8
11
|
const PROXY_ENV_KEYS = [
|
|
@@ -21,6 +24,112 @@ export interface CodexShimReadinessInputs {
|
|
|
21
24
|
configuredProxyResolved: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
export type CodexConnectShimStatus = "ready" | "missing" | "unhealthy" | "unverified";
|
|
28
|
+
|
|
29
|
+
export interface CodexConnectShimReadiness {
|
|
30
|
+
status: CodexConnectShimStatus;
|
|
31
|
+
/** Secret-free, actionable text rendered by ocx connect. */
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CodexConnectShimInspectionDeps {
|
|
36
|
+
diagnose?: () => Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
37
|
+
findOnPath?: () => CodexPathCandidate | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CODEX_TOKEN_NOTE = "The connected Codex config uses OPENCODEX_API_AUTH_TOKEN; "
|
|
41
|
+
+ "without a working shim or when a PATH wrapper replaces it, Codex may fail with "
|
|
42
|
+
+ '"Missing environment variable".';
|
|
43
|
+
const CODEX_TOKEN_ACTION = `${CODEX_TOKEN_NOTE} Run 'ocx codex-shim install' to repair it.`;
|
|
44
|
+
|
|
45
|
+
export function codexConnectShimReadiness(inputs: {
|
|
46
|
+
diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
47
|
+
commandPath: string | null;
|
|
48
|
+
commandIsShim?: boolean;
|
|
49
|
+
}): CodexConnectShimReadiness {
|
|
50
|
+
if (inputs.diagnosis.runnable && inputs.diagnosis.active !== undefined) {
|
|
51
|
+
if (inputs.diagnosis.active === null) {
|
|
52
|
+
return {
|
|
53
|
+
status: "unverified",
|
|
54
|
+
message: `PATH activation could not be verified. Shim state: ${inputs.diagnosis.summary}. `
|
|
55
|
+
+ `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!inputs.diagnosis.active) {
|
|
59
|
+
return {
|
|
60
|
+
status: "missing",
|
|
61
|
+
message: `installed but not active; ${inputs.commandPath
|
|
62
|
+
? `PATH resolves 'codex' to ${inputs.commandPath}`
|
|
63
|
+
: "no 'codex' executable was found on PATH"}. `
|
|
64
|
+
+ `Shim state: ${inputs.diagnosis.summary}. ${overlayActivationHint()} ${CODEX_TOKEN_NOTE}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { status: "ready", message: "installed and healthy" };
|
|
68
|
+
}
|
|
69
|
+
if (inputs.diagnosis.installed && !inputs.diagnosis.healthy) {
|
|
70
|
+
return {
|
|
71
|
+
status: "unhealthy",
|
|
72
|
+
message: `installed but unhealthy: ${inputs.diagnosis.summary}. ${CODEX_TOKEN_ACTION}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (inputs.diagnosis.healthy && inputs.commandPath && !inputs.commandIsShim) {
|
|
76
|
+
// The shim is installed and healthy, so reinstalling it cannot fix this: only PATH
|
|
77
|
+
// order (or the wrapper in front of it) can.
|
|
78
|
+
return {
|
|
79
|
+
status: "missing",
|
|
80
|
+
message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. `
|
|
81
|
+
+ `Shim state: ${inputs.diagnosis.summary}. Put the directory of the tracked shim ahead of that command on PATH, `
|
|
82
|
+
+ `or remove that wrapper; re-running 'ocx codex-shim install' will not change PATH order. ${CODEX_TOKEN_NOTE}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (inputs.diagnosis.installed && inputs.diagnosis.healthy) {
|
|
86
|
+
if (!inputs.commandPath) {
|
|
87
|
+
return {
|
|
88
|
+
status: "missing",
|
|
89
|
+
message: `installed but not active; no 'codex' executable was found on PATH. `
|
|
90
|
+
+ `Shim state: ${inputs.diagnosis.summary}. Add the directory of the tracked shim to PATH. ${CODEX_TOKEN_NOTE}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { status: "ready", message: "installed and healthy" };
|
|
94
|
+
}
|
|
95
|
+
if (inputs.commandPath) {
|
|
96
|
+
return {
|
|
97
|
+
status: "missing",
|
|
98
|
+
message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. ${CODEX_TOKEN_ACTION}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
status: "missing",
|
|
103
|
+
message: `not installed and no 'codex' executable was found on PATH. ${CODEX_TOKEN_ACTION}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function inspectCodexShimForConnect(
|
|
108
|
+
deps: CodexConnectShimInspectionDeps = {},
|
|
109
|
+
): CodexConnectShimReadiness {
|
|
110
|
+
let diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
111
|
+
try {
|
|
112
|
+
diagnosis = (deps.diagnose ?? diagnoseCodexShim)();
|
|
113
|
+
} catch {
|
|
114
|
+
diagnosis = { installed: true, healthy: false, summary: "diagnostic state could not be read" };
|
|
115
|
+
}
|
|
116
|
+
let command: CodexPathCandidate | null = null;
|
|
117
|
+
try {
|
|
118
|
+
command = (deps.findOnPath ?? findFirstCodexOnPath)();
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
status: "unverified",
|
|
122
|
+
message: `PATH activation could not be verified. Shim state: ${diagnosis.summary}. `
|
|
123
|
+
+ `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return codexConnectShimReadiness({
|
|
127
|
+
diagnosis,
|
|
128
|
+
commandPath: command?.path ?? null,
|
|
129
|
+
commandIsShim: command?.isShim ?? false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
24
133
|
function externalProviderLabel(provider: string | null): string {
|
|
25
134
|
return provider
|
|
26
135
|
? `external model_provider ${JSON.stringify(provider)}`
|