@bitkyc08/opencodex 2.42.0 → 2.43.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.
Files changed (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -1,5 +1,7 @@
1
1
  import { writeSync } from "node:fs";
2
+ import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance";
2
3
  import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
4
+ import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
3
5
  import {
4
6
  CliUsageError,
5
7
  printData,
@@ -34,7 +36,7 @@ const USAGE = `Usage:
34
36
  ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--code -] [--no-wait] [--json]
35
37
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
36
38
  ocx account cancel <provider> [--flow <flow-id>] [--json]
37
- ocx account reset-credits <account-id|main> [--consume --yes] [--json]
39
+ ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
38
40
 
39
41
  --device runs the OpenAI device-code login instead of the browser callback: use
40
42
  it when the proxy has no browser or nothing can reach localhost:1455, such as a
@@ -136,7 +138,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
136
138
  }, deps);
137
139
  }
138
140
  if (noWait) {
139
- if (wantsJson) printData(start, true);
141
+ printData({ ...start, modelSelection: modelSelectionNextSteps("openai", true) }, wantsJson, modelSelectionGuidance("openai", true));
140
142
  return;
141
143
  }
142
144
  if (!start.flowId) throw new CliUsageError("login did not return a flow id");
@@ -152,7 +154,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
152
154
  {}, deps,
153
155
  );
154
156
  if (state.status === "done") {
155
- printData(state, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`]);
157
+ printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
156
158
  if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
157
159
  return;
158
160
  }
@@ -183,7 +185,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
183
185
  }, deps);
184
186
  }
185
187
  if (noWait) {
186
- if (wantsJson) printData(start, true);
188
+ printData({ ...start, modelSelection: modelSelectionNextSteps(provider, true) }, wantsJson, modelSelectionGuidance(provider, true));
187
189
  return;
188
190
  }
189
191
  for (let attempt = 0; attempt < 100; attempt++) {
@@ -191,7 +193,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
191
193
  const state = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, {}, deps);
192
194
  if (state.error) throw new CliUsageError(String(state.error));
193
195
  if (state.loggedIn === true) {
194
- printData(state, wantsJson, [`Logged in to ${provider}.`]);
196
+ printData({ ...state, modelSelection: modelSelectionNextSteps(provider) }, wantsJson, [`Logged in to ${provider}.`, ...modelSelectionGuidance(provider)]);
195
197
  return;
196
198
  }
197
199
  }
@@ -252,12 +254,25 @@ async function resetCredits(argv: string[], deps: RuntimeApiDeps): Promise<void>
252
254
  const wantsJson = takeFlag(args, "--json");
253
255
  const consume = takeFlag(args, "--consume");
254
256
  const yes = takeFlag(args, "--yes");
257
+ // Before rejectArgs: takeOption splices its two tokens out of `args`.
258
+ const operationId = takeOption(args, "--operation-id");
255
259
  if (!rawId) throw new CliUsageError("account id is required", USAGE);
256
260
  if (consume && !yes) throw new CliUsageError("consuming a reset credit requires --yes", USAGE);
261
+ if (operationId !== undefined && !consume) {
262
+ throw new CliUsageError("--operation-id requires --consume", USAGE);
263
+ }
264
+ if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
265
+ throw new CliUsageError("--operation-id must be a UUIDv4", USAGE);
266
+ }
257
267
  rejectArgs(args, USAGE);
258
268
  const accountId = rawId === "main" ? "__main__" : rawId;
259
269
  const result = consume
260
- ? await runtimeRequest("/api/codex-auth/reset-credits/consume", { method: "POST", body: JSON.stringify({ accountId }) }, deps)
270
+ ? await runtimeRequest("/api/codex-auth/reset-credits/consume", {
271
+ method: "POST",
272
+ // Spread, not `operationId: undefined`: the server distinguishes an absent
273
+ // key (legacy random id) from a caller who asked for a stable identity.
274
+ body: JSON.stringify({ accountId, ...(operationId === undefined ? {} : { operationId }) }),
275
+ }, deps)
261
276
  : await runtimeRequest(`/api/codex-auth/reset-credits?accountId=${encodeURIComponent(accountId)}`, {}, deps);
262
277
  printData(result, wantsJson);
263
278
  }
@@ -11,7 +11,7 @@
11
11
  * top-level `const USAGE`, evaluated at import time, so a cycle back into this table
12
12
  * would resolve to `undefined` under ESM rather than throwing -- silently emptying the
13
13
  * usage text that `rejectArgs` hands to `CliUsageError`, in the exact error-reporting
14
- * surface the CLI-operability issues are about. `tests/cli-capabilities.test.ts` asserts
14
+ * surface the CLI-operability issues are about. `tests/cli/cli-capabilities.test.ts` asserts
15
15
  * the absence of those imports and that every rendered usage string is non-empty, so the
16
16
  * failure mode is loud instead of degraded.
17
17
  *
@@ -19,7 +19,7 @@
19
19
  * `HEAD_CAPABILITIES`. They exit in the CLI head (`root.ts`) before dispatch and have no
20
20
  * runner key, so listing them as ordinary capabilities would break the registry parity
21
21
  * assertion that every canonical entry is a direct runner. `help` is excluded from
22
- * `CLI_COMMANDS` deliberately -- `tests/cli-registry.test.ts` documents it as a
22
+ * `CLI_COMMANDS` deliberately -- `tests/cli/cli-registry.test.ts` documents it as a
23
23
  * head-handled pseudo-case -- and that decision is preserved here rather than reversed.
24
24
  */
25
25
 
@@ -153,6 +153,17 @@ export const CAPABILITIES: readonly Capability[] = [
153
153
  json: "envelope",
154
154
  details: ["Reads local config; drives no management API route."],
155
155
  },
156
+ {
157
+ command: ["provider", "resets"],
158
+ summary: "Recently detected quota resets and whether reset notifications are enabled.",
159
+ routes: [{ method: "GET", path: "/api/quota-resets" }],
160
+ flags: [
161
+ { name: "--json", value: "boolean", summary: "Emit reset events as JSON." },
162
+ { name: "--limit", value: "number", summary: "Limit returned events; defaults to 20, capped at 100." },
163
+ ],
164
+ mutates: false,
165
+ json: "payload",
166
+ },
156
167
  {
157
168
  command: ["provider", "keychain"],
158
169
  summary: "Move a provider's API key into the OS keychain, restore it, or report where it lives.",
package/src/cli/claude.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * `ocx claude [claude args...]` — launch Claude Code wired to the local proxy.
2
+ * `ocx claude [claude args...]` — launch Claude Code through the local proxy,
3
+ * or natively when Claude routing is explicitly disabled.
3
4
  *
4
5
  * Mirrors `ccr code` UX (devlog/260711_claude_inbound/020, 003 E1/E2/E5/G1):
5
6
  * ensures the proxy is running, injects the Anthropic env slots, then execs the
@@ -9,8 +10,9 @@
9
10
  import { spawn } from "node:child_process";
10
11
  import { loadConfig } from "../config";
11
12
  import { injectClaudeAgentDefs } from "../claude/agents-inject";
13
+ import { CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
12
14
  import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
13
- import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
15
+ import { claudeConfigDir, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
14
16
  import { commandInvocation } from "../lib/win-exec";
15
17
  import { isProxyAdmissionSecret } from "../server/auth-cors";
16
18
  import { findLiveProxy } from "../server/proxy-liveness";
@@ -21,10 +23,11 @@ import { resolveClaudeAuthMode } from "../claude/auth-mode";
21
23
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
22
24
  import { selfLaunchArgv } from "../lib/self-launch-argv";
23
25
  import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "./launcher-context";
24
- import { readClientConnectionState } from "../client/state";
25
- import { readServiceApiTokenState } from "../lib/service-secrets";
26
+ import { readClientConnectionState, type ClientConnectionState } from "../client/state";
27
+ import { readServiceApiTokenState, type ServiceApiTokenState } from "../lib/service-secrets";
26
28
  import { DEFAULT_CATALOG_PATH } from "../codex/paths";
27
29
  import { readFileSync } from "node:fs";
30
+ import { join } from "node:path";
28
31
  import { aliasForNative, aliasForRoute } from "../claude/alias";
29
32
  import { desktop3pAlias } from "../claude/desktop-3p";
30
33
 
@@ -49,6 +52,48 @@ export type ClaudeEnvDeps = {
49
52
  allowRootSkipPermissions?: boolean;
50
53
  };
51
54
 
55
+ function deleteUntrustedAnthropicSlots(env: ClaudeLaunchEnv, deps: ClaudeEnvDeps): void {
56
+ const explicitSlots = deps.preBunAnthropicSlots;
57
+ const trustedSlots = explicitSlots === undefined
58
+ ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
59
+ : explicitSlots ?? [];
60
+ const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
61
+ for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
62
+ const value = env[name];
63
+ if (value !== undefined && value !== "" && !exported.has(name)) delete env[name];
64
+ }
65
+ delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
66
+ delete env.OCX_NODE_LAUNCH_CONTEXT;
67
+ }
68
+
69
+ /**
70
+ * Read Claude Code's own persisted `/model` picker default.
71
+ *
72
+ * An absent `settings.json` is the ordinary fresh-install case and stays silent. A
73
+ * present-but-unparseable one is not: swallowing it would drop the "saved model requires
74
+ * the proxy" warning exactly when the file is broken, so the native session would start
75
+ * on a model the user never chose with no explanation. Name the file, never its contents.
76
+ */
77
+ export function readPickerDefaultModel(configDir: string): string | null {
78
+ const file = join(configDir, "settings.json");
79
+ let raw: string;
80
+ try {
81
+ raw = readFileSync(file, "utf8");
82
+ } catch (err) { // no-excuse-ok: catch -- an absent picker settings file is the default install state.
83
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
84
+ console.warn(`⚠ Could not read Claude Code settings at ${file}; the saved model check is skipped this run.`);
85
+ }
86
+ return null;
87
+ }
88
+ try {
89
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
90
+ return typeof parsed.model === "string" && parsed.model.trim() !== "" ? parsed.model.trim() : null;
91
+ } catch { // no-excuse-ok: catch -- a corrupt picker file must warn, not abort the launch.
92
+ console.warn(`⚠ Claude Code settings at ${file} are not valid JSON; the saved model check is skipped this run.`);
93
+ return null;
94
+ }
95
+ }
96
+
52
97
  function isClaudeLoopbackHostname(hostname: string): boolean {
53
98
  const normalized = hostname.toLowerCase().replace(/\.$/, "");
54
99
  return normalized === "localhost"
@@ -128,18 +173,7 @@ export function buildClaudeEnv(
128
173
  // Direct `bun src/cli/index.ts` therefore loses ambient Anthropic values. That is a
129
174
  // real cost to a documented entry point, and the escape hatch is the launcher: run
130
175
  // through `ocx` (the published bin) and genuine shell exports are preserved by proof.
131
- const explicitSlots = deps.preBunAnthropicSlots;
132
- const trustedSlots = explicitSlots === undefined
133
- ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
134
- : explicitSlots ?? [];
135
- const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
136
- for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
137
- const value = env[name];
138
- if (value !== undefined && value !== "" && !exported.has(name)) delete env[name];
139
- }
140
- // Never forward old or current provenance seams to Claude Code.
141
- delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
142
- delete env.OCX_NODE_LAUNCH_CONTEXT;
176
+ deleteUntrustedAnthropicSlots(env, deps);
143
177
  const setDefault = (name: string, value: string | undefined) => {
144
178
  if (value === undefined || value.length === 0) return;
145
179
  if (env[name] !== undefined && env[name] !== "") return; // user wins
@@ -302,7 +336,12 @@ export function buildClaudeEnv(
302
336
  * daemon registers every selector form — audit R3#1). 3s bound + management auth header.
303
337
  * (no [1m] marking, conservative).
304
338
  */
305
- export async function fetchClaudeContextWindows(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<Record<string, number>> {
339
+ export interface ClaudeCodeLiveState {
340
+ contextWindows: Record<string, number>;
341
+ enabled?: boolean;
342
+ }
343
+
344
+ export async function fetchClaudeCodeState(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<ClaudeCodeLiveState> {
306
345
  try {
307
346
  const headers = new Headers();
308
347
  const token = configuredAdminToken();
@@ -311,15 +350,22 @@ export async function fetchClaudeContextWindows(config: OcxConfig, port: number,
311
350
  headers,
312
351
  signal: AbortSignal.timeout(timeoutMs),
313
352
  });
314
- if (!res.ok) return {};
315
- const body = await res.json() as { contextWindows?: Record<string, number> };
316
- return body.contextWindows && typeof body.contextWindows === "object" ? body.contextWindows : {};
353
+ if (!res.ok) return { contextWindows: {} };
354
+ const body = await res.json() as { contextWindows?: Record<string, number>; enabled?: boolean };
355
+ return {
356
+ contextWindows: body.contextWindows && typeof body.contextWindows === "object" ? body.contextWindows : {},
357
+ ...(typeof body.enabled === "boolean" ? { enabled: body.enabled } : {}),
358
+ };
317
359
  } catch {
318
360
  console.error("⚠ 모델 컨텍스트 정보를 불러오지 못했습니다 — 1M 자동 표시는 이번 실행에서 생략됩니다.");
319
- return {};
361
+ return { contextWindows: {} };
320
362
  }
321
363
  }
322
364
 
365
+ export async function fetchClaudeContextWindows(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<Record<string, number>> {
366
+ return (await fetchClaudeCodeState(config, port, timeoutMs)).contextWindows;
367
+ }
368
+
323
369
  export function readConnectedClaudeContextWindows(path = DEFAULT_CATALOG_PATH): Record<string, number> {
324
370
  try {
325
371
  const parsed = JSON.parse(readFileSync(path, "utf8")) as { models?: unknown };
@@ -384,6 +430,139 @@ export async function ensureProxyForClaude(deps: ClaudeProxyEnsureDeps = {}): Pr
384
430
  return null;
385
431
  }
386
432
 
433
+ export const CLAUDE_NATIVE_ROUTING_OFF =
434
+ "ℹ️ Claude Code routing is disabled in OpenCodex. Launching Claude Code natively. Enable Claude routing to use the proxy again.";
435
+
436
+ export const CLAUDE_NATIVE_LIVE_DISABLED =
437
+ "ℹ️ The running OpenCodex proxy has Claude Code routing disabled. Launching Claude Code natively. Restart the service after enabling routing.";
438
+
439
+ export type ClaudeLaunchPlan =
440
+ | { kind: "routed" }
441
+ | { kind: "native"; notice: string };
442
+
443
+ export function claudeLaunchPlan(
444
+ configuredEnabled: boolean,
445
+ liveEnabled: boolean | undefined,
446
+ ): ClaudeLaunchPlan {
447
+ if (!configuredEnabled) return { kind: "native", notice: CLAUDE_NATIVE_ROUTING_OFF };
448
+ if (liveEnabled === false) return { kind: "native", notice: CLAUDE_NATIVE_LIVE_DISABLED };
449
+ return { kind: "routed" };
450
+ }
451
+
452
+ export type ClaudeLaunchPreflight =
453
+ | { kind: "continue" }
454
+ | { kind: "native"; notice: string }
455
+ | { kind: "error"; message: string };
456
+
457
+ /** Validate connected-client ownership before any native fallback can run. */
458
+ export function claudeLaunchPreflight(
459
+ configuredEnabled: boolean,
460
+ clientState: ClientConnectionState,
461
+ tokenState?: ServiceApiTokenState,
462
+ ): ClaudeLaunchPreflight {
463
+ if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
464
+ return { kind: "error", message: `Client state is ${clientState.kind}: ${clientState.reason}` };
465
+ }
466
+ if (clientState.kind === "connected") {
467
+ if (!clientState.value.selectedClients.includes("claude")) {
468
+ return { kind: "error", message: "Claude is not selected for this remote hub connection." };
469
+ }
470
+ if (tokenState?.kind !== "present" || tokenState.fingerprint !== clientState.value.tokenFingerprint) {
471
+ return {
472
+ kind: "error",
473
+ message: tokenState?.kind === "absent"
474
+ ? "Connected service token is missing."
475
+ : "Connected service token ownership changed.",
476
+ };
477
+ }
478
+ }
479
+ return configuredEnabled
480
+ ? { kind: "continue" }
481
+ : { kind: "native", notice: CLAUDE_NATIVE_ROUTING_OFF };
482
+ }
483
+
484
+ const NATIVE_STRIPPED_LEVERS = [
485
+ "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
486
+ "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
487
+ "CLAUDE_CODE_MAX_CONTEXT_TOKENS",
488
+ "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
489
+ "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
490
+ "DISABLE_COMPACT",
491
+ ] as const;
492
+
493
+ const MODEL_ENV_SLOT_NAMES = [
494
+ "ANTHROPIC_MODEL",
495
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
496
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
497
+ "ANTHROPIC_DEFAULT_FABLE_MODEL",
498
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
499
+ "ANTHROPIC_SMALL_FAST_MODEL",
500
+ ] as const;
501
+
502
+ const DESKTOP_3P_ALIAS = /^claude-opus-4(?:-8)?-[a-z][0-9a-z]{2}$/;
503
+
504
+ export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = []): boolean {
505
+ const id = value.trim().replace(/\[1m\]$/, "");
506
+ if (!id) return false;
507
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1) || id.startsWith(CLAUDE_ALIAS_PREFIX_V2) || DESKTOP_3P_ALIAS.test(id)) {
508
+ return true;
509
+ }
510
+ const slash = id.indexOf("/");
511
+ return slash > 0 && providerNames.includes(id.slice(0, slash));
512
+ }
513
+
514
+ export function buildNativeClaudeEnv(
515
+ config: OcxConfig,
516
+ base: ClaudeLaunchEnv,
517
+ deps: ClaudeEnvDeps = {},
518
+ ): ClaudeLaunchEnv {
519
+ const env: ClaudeLaunchEnv = { ...base };
520
+ deleteUntrustedAnthropicSlots(env, deps);
521
+
522
+ const admissionSlots = ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] as const;
523
+ const hasOwnedAdmission = admissionSlots.some(name => {
524
+ const value = env[name]?.trim();
525
+ return Boolean(value && (value === PROXY_MARKER || isProxyAdmissionSecret(value, config)));
526
+ });
527
+ const baseUrl = env.ANTHROPIC_BASE_URL;
528
+ if (hasOwnedAdmission && targetsLocalClaudeProxy(baseUrl, config.port)) {
529
+ delete env.ANTHROPIC_BASE_URL;
530
+ }
531
+ for (const name of admissionSlots) {
532
+ const value = env[name]?.trim();
533
+ if (value && (value === PROXY_MARKER || isProxyAdmissionSecret(value, config))) delete env[name];
534
+ }
535
+
536
+ for (const name of NATIVE_STRIPPED_LEVERS) delete env[name];
537
+ const providerNames = Object.keys(config.providers);
538
+ for (const name of MODEL_ENV_SLOT_NAMES) {
539
+ const value = env[name];
540
+ if (value && isProxyOnlyModelId(value, providerNames)) delete env[name];
541
+ }
542
+ if (deps.allowRootSkipPermissions === true && !env.IS_SANDBOX) env.IS_SANDBOX = "1";
543
+ return env;
544
+ }
545
+
546
+ export function nativeModelOverride(
547
+ pickedModel: string | null,
548
+ configuredModel: string | undefined,
549
+ args: readonly string[],
550
+ providerNames: readonly string[] = [],
551
+ ): { flag?: string[]; warning?: string } {
552
+ if (!pickedModel || !isProxyOnlyModelId(pickedModel, providerNames)) return {};
553
+ if (args.some(arg => arg === "--model" || arg.startsWith("--model="))) return {};
554
+ const fallback = configuredModel?.trim();
555
+ if (fallback && !isProxyOnlyModelId(fallback, providerNames)) {
556
+ return {
557
+ flag: ["--model", fallback],
558
+ warning: `ℹ️ The saved model (${pickedModel}) requires the proxy. This native session will use ${fallback}.`,
559
+ };
560
+ }
561
+ return {
562
+ warning: `⚠ The saved model (${pickedModel}) requires the proxy. Use \`--model <Anthropic model>\` or select a native model in this session.`,
563
+ };
564
+ }
565
+
387
566
  const CLAUDE_INSTALL_HINT = "❌ `claude` CLI not found. Install it first: npm install -g @anthropic-ai/claude-code";
388
567
 
389
568
  /**
@@ -417,28 +596,19 @@ export function rootSkipPermissionsNotice(env: ClaudeLaunchEnv): string {
417
596
 
418
597
  export async function cmdClaude(args: string[]): Promise<number> {
419
598
  const config = loadConfig();
420
- if (config.claudeCode?.enabled === false) {
421
- console.error("Claude inbound is disabled (config.claudeCode.enabled=false — flip the Claude ON toggle in the GUI or edit config).");
422
- return 1;
423
- }
424
599
  const clientState = readClientConnectionState();
425
- if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
426
- console.error(`Client state is ${clientState.kind}: ${clientState.reason}`);
600
+ const tokenState = clientState.kind === "connected" ? readServiceApiTokenState() : undefined;
601
+ const preflight = claudeLaunchPreflight(config.claudeCode?.enabled !== false, clientState, tokenState);
602
+ if (preflight.kind === "error") {
603
+ console.error(preflight.message);
427
604
  return 1;
428
605
  }
606
+ if (preflight.kind === "native") return launchNativeClaude(config, args, preflight.notice);
429
607
  let route: number | ClaudeRoutingTarget;
430
608
  let contextWindows: Record<string, number>;
431
609
  if (clientState.kind === "connected") {
432
- if (!clientState.value.selectedClients.includes("claude")) {
433
- console.error("Claude is not selected for this remote hub connection.");
434
- return 1;
435
- }
436
- const token = readServiceApiTokenState();
437
- if (token.kind !== "present" || token.fingerprint !== clientState.value.tokenFingerprint) {
438
- console.error(token.kind === "absent" ? "Connected service token is missing." : "Connected service token ownership changed.");
439
- return 1;
440
- }
441
- route = { baseUrl: clientState.value.serverUrl, admissionToken: token.token };
610
+ if (tokenState?.kind !== "present") return 1;
611
+ route = { baseUrl: clientState.value.serverUrl, admissionToken: tokenState.token };
442
612
  contextWindows = readConnectedClaudeContextWindows();
443
613
  } else {
444
614
  const port = await ensureProxyForClaude();
@@ -446,8 +616,11 @@ export async function cmdClaude(args: string[]): Promise<number> {
446
616
  console.error("❌ Proxy did not become healthy after starting.");
447
617
  return 1;
448
618
  }
619
+ const liveState = await fetchClaudeCodeState(config, port);
620
+ const plan = claudeLaunchPlan(true, liveState.enabled);
621
+ if (plan.kind === "native") return launchNativeClaude(config, args, plan.notice);
449
622
  route = port;
450
- contextWindows = await fetchClaudeContextWindows(config, port);
623
+ contextWindows = liveState.contextWindows;
451
624
  }
452
625
  const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
453
626
  const env = buildClaudeEnv(config, route, process.env, contextWindows, { allowRootSkipPermissions });
@@ -479,7 +652,27 @@ export async function cmdClaude(args: string[]): Promise<number> {
479
652
  console.error(`⚠ Claude agent definitions could not be synced: ${message}`);
480
653
  }
481
654
  }
482
- return await new Promise<number>(resolve => {
655
+ return spawnClaude(args, env);
656
+ }
657
+
658
+ async function launchNativeClaude(config: OcxConfig, args: string[], notice: string): Promise<number> {
659
+ console.error(notice);
660
+ const providerNames = Object.keys(config.providers);
661
+ const override = nativeModelOverride(
662
+ readPickerDefaultModel(claudeConfigDir()),
663
+ config.claudeCode?.model,
664
+ args,
665
+ providerNames,
666
+ );
667
+ if (override.warning) console.error(override.warning);
668
+ const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
669
+ const env = buildNativeClaudeEnv(config, process.env, { allowRootSkipPermissions });
670
+ if (allowRootSkipPermissions) console.error(rootSkipPermissionsNotice(env));
671
+ return spawnClaude([...(override.flag ?? []), ...args], env);
672
+ }
673
+
674
+ function spawnClaude(args: string[], env: ClaudeLaunchEnv): Promise<number> {
675
+ return new Promise<number>(resolve => {
483
676
  const inv = commandInvocation("claude", args);
484
677
  const child = spawn(inv.file, inv.args, { stdio: "inherit", env: env as NodeJS.ProcessEnv, ...inv.options });
485
678
  child.on("error", (err: NodeJS.ErrnoException) => {
@@ -15,7 +15,15 @@ const USAGE = `Usage:
15
15
  ocx config export <path|->
16
16
  ocx config import <path|-> --yes [--json]`;
17
17
 
18
- const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret)$/i;
18
+ /**
19
+ * Keys whose VALUE is a credential and must never be printed or exported.
20
+ *
21
+ * `webhookUrl` is here because for Slack and Discord the URL itself is the authorization:
22
+ * anyone holding it can post to the channel. It looks like configuration rather than a secret,
23
+ * which is exactly why it needs to be named explicitly — none of the other patterns match it,
24
+ * so `ocx config show` printed it and `config export` wrote it to disk in the clear.
25
+ */
26
+ const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl)$/i;
19
27
  const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
20
28
 
21
29
  function redact(value: unknown, key = ""): unknown {
@@ -349,7 +349,7 @@ const commandRunners: Record<string, CommandRunner> = {
349
349
  : "Remote hub catalog synchronized.");
350
350
  await handleConnectedSyncCatalogWrite(result, restartCodex, restartDesktopApp);
351
351
  // `process.exitCode` rather than a literal 0, for the same reason every other
352
- // runner does it (tests/cli-transport-honesty.test.ts): the catalog-write helper
352
+ // runner does it (tests/cli/cli-transport-honesty.test.ts): the catalog-write helper
353
353
  // drives app-server restarts, and one of those recording a failure must not be
354
354
  // erased by the value this runner returns. It reads 0 on the ordinary path. Node
355
355
  // types it as `number | string`; only a numeric code means anything here.
@@ -698,6 +698,10 @@ const commandRunners: Record<string, CommandRunner> = {
698
698
  return await handleRoutePolicyCommand(deps.args.slice(2));
699
699
  }
700
700
  },
701
+ effort: async deps => {
702
+ const { handleEffortCommand } = await import("./effort");
703
+ return await handleEffortCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
704
+ },
701
705
  agent: async deps => {
702
706
  const { handleAgentCommand } = await import("./agent");
703
707
  return await handleAgentCommand(deps.args.slice(1));
package/src/cli/doctor.ts CHANGED
@@ -46,6 +46,10 @@ import {
46
46
  resolveEffectiveUserIdentity,
47
47
  } from "../codex/user-identity";
48
48
  import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
49
+ import {
50
+ collectLegacyCodexConfigKeyDiagnostics,
51
+ formatLegacyCodexConfigKeyDiagnosticsForDoctor,
52
+ } from "../codex/legacy-config-keys";
49
53
  import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
50
54
  import {
51
55
  displayCodexRuntimePath,
@@ -1270,6 +1274,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1270
1274
  }
1271
1275
  }
1272
1276
 
1277
+ console.log("\nCodex config compatibility");
1278
+ const legacyKeyResult = collectLegacyCodexConfigKeyDiagnostics();
1279
+ for (const line of formatLegacyCodexConfigKeyDiagnosticsForDoctor(legacyKeyResult)) {
1280
+ console.log(line);
1281
+ }
1282
+
1273
1283
  console.log("\nCodex agent role files");
1274
1284
  const tomlFallbackRoles = scanCodexAgentRolesWithTomlModelFallback(resolveCodexHomeDirImpl());
1275
1285
  if (tomlFallbackRoles.length === 0) {