@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) 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-VGeQEZ_v.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 +54 -24
  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/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
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) {