@bitkyc08/opencodex 2.76.0 → 2.77.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 (189) hide show
  1. package/gui/dist/assets/App-COdEeqSj.css +1 -0
  2. package/gui/dist/assets/App-DEpCJEP_.js +54 -0
  3. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
  4. package/gui/dist/assets/index-Cd7ittMv.css +1 -0
  5. package/gui/dist/assets/index-DwJKFJm5.js +94 -0
  6. package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
  7. package/gui/dist/index.html +4 -3
  8. package/package.json +1 -1
  9. package/src/adapters/antigravity-refusal.ts +28 -0
  10. package/src/adapters/cursor/protobuf-request.ts +6 -0
  11. package/src/adapters/cursor/request-builder.ts +3 -0
  12. package/src/adapters/cursor/structured-output.ts +13 -0
  13. package/src/adapters/cursor/types.ts +2 -0
  14. package/src/adapters/ollama-native.ts +29 -2
  15. package/src/adapters/openai-responses/passthrough.ts +133 -103
  16. package/src/adapters/openai-responses/request-strips.ts +10 -4
  17. package/src/adapters/zed.ts +26 -4
  18. package/src/bridge/response-json.ts +14 -14
  19. package/src/bridge/sse.ts +15 -5
  20. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  21. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  22. package/src/claude/agents-inject.ts +4 -43
  23. package/src/claude/gateway-cache.ts +16 -13
  24. package/src/claude/inbound-model-options.ts +9 -2
  25. package/src/claude/intercept/cli-catalog.ts +27 -16
  26. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  27. package/src/claude/intercept/picker-budget.ts +41 -0
  28. package/src/claude/outbound.ts +2 -0
  29. package/src/claude/subagent-force-status.ts +53 -0
  30. package/src/claude/subagent-model.ts +93 -0
  31. package/src/cli/account-api.ts +3 -0
  32. package/src/cli/account.ts +3 -1
  33. package/src/cli/agent.ts +10 -0
  34. package/src/cli/capabilities.ts +9 -0
  35. package/src/cli/chatgpt-command.ts +10 -7
  36. package/src/cli/claude.ts +22 -6
  37. package/src/cli/codex-shim-readiness.ts +89 -0
  38. package/src/cli/connect.ts +19 -0
  39. package/src/cli/dispatch.ts +3 -3
  40. package/src/cli/export-command.ts +20 -9
  41. package/src/cli/gui.ts +8 -3
  42. package/src/cli/help-catalog.ts +84 -0
  43. package/src/cli/help-models-context.ts +17 -0
  44. package/src/cli/help-navigation.ts +50 -0
  45. package/src/cli/help-recovery.ts +66 -0
  46. package/src/cli/help.ts +52 -8
  47. package/src/cli/models-runtime.ts +2 -1
  48. package/src/cli/opencode.ts +54 -10
  49. package/src/cli/provider.ts +3 -29
  50. package/src/cli/registry.ts +29 -5
  51. package/src/cli/root.ts +30 -9
  52. package/src/cli/runtime-api.ts +1 -1
  53. package/src/cli/tray-proxy.ts +3 -2
  54. package/src/clients/config-export/contracts.ts +100 -0
  55. package/src/clients/config-export/droid.ts +54 -2
  56. package/src/clients/config-export/kilo.ts +12 -2
  57. package/src/clients/config-export/model-metadata.ts +10 -4
  58. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  59. package/src/clients/config-export.ts +46 -66
  60. package/src/clients/omo-role-models.ts +17 -3
  61. package/src/codex/account-credit-use.ts +4 -2
  62. package/src/codex/account-runtime-state.ts +21 -0
  63. package/src/codex/account-store.ts +11 -47
  64. package/src/codex/account-usability.ts +11 -8
  65. package/src/codex/auth-api/account-list.ts +73 -63
  66. package/src/codex/auth-api/main-account-probe.ts +25 -12
  67. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  68. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  69. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  70. package/src/codex/auth-context.ts +30 -11
  71. package/src/codex/catalog/aggregation.ts +3 -0
  72. package/src/codex/catalog/model-hints.ts +2 -1
  73. package/src/codex/catalog/model-visibility.ts +6 -3
  74. package/src/codex/catalog/parsing.ts +2 -0
  75. package/src/codex/catalog/provider-models.ts +13 -4
  76. package/src/codex/catalog/retained-sync.ts +2 -1
  77. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  78. package/src/codex/convergence.ts +2 -1
  79. package/src/codex/main-account.ts +144 -11
  80. package/src/codex/quota-refresh-outcome.ts +17 -2
  81. package/src/codex/quota-types.ts +33 -1
  82. package/src/codex/quota.ts +56 -15
  83. package/src/codex/routing/cooldown-math.ts +10 -4
  84. package/src/codex/routing/selection.ts +11 -4
  85. package/src/codex/routing.ts +9 -9
  86. package/src/codex/shim-path-resolution.ts +133 -0
  87. package/src/codex/shim.ts +39 -39
  88. package/src/codex/subagent-model-fallback.ts +2 -1
  89. package/src/combos/failover.ts +82 -1
  90. package/src/combos/jev.ts +13 -3
  91. package/src/config/diagnostics.ts +4 -0
  92. package/src/config/load-degrade.ts +4 -0
  93. package/src/config/provider-validation.ts +3 -0
  94. package/src/config/schema/compaction-triggers.ts +8 -0
  95. package/src/config/schema/config-schema.ts +11 -0
  96. package/src/config/schema/leaf-validators.ts +5 -1
  97. package/src/config/subagent-models.ts +12 -0
  98. package/src/generated/compatibility-version.json +245 -153
  99. package/src/grok/session-identity.ts +33 -0
  100. package/src/integrations/catalog-refresh.ts +1 -1
  101. package/src/integrations/mutation-plan.ts +4 -2
  102. package/src/integrations/state.ts +68 -6
  103. package/src/integrations/writer.ts +18 -4
  104. package/src/lib/gui-pair-capability.ts +17 -0
  105. package/src/lib/provider-client-headers.ts +62 -0
  106. package/src/oauth/anthropic-continuity.ts +65 -0
  107. package/src/oauth/anthropic-identity.ts +75 -0
  108. package/src/oauth/anthropic-routing.ts +3 -3
  109. package/src/oauth/anthropic.ts +7 -1
  110. package/src/oauth/generic-account-failover.ts +21 -0
  111. package/src/oauth/health.ts +32 -5
  112. package/src/oauth/index.ts +36 -27
  113. package/src/oauth/local-token-detect.ts +7 -0
  114. package/src/oauth/refresh-policy.ts +2 -0
  115. package/src/oauth/store.ts +55 -6
  116. package/src/oauth/types.ts +9 -0
  117. package/src/protocols/encoders/messages.ts +4 -3
  118. package/src/providers/antigravity-effort-families.ts +75 -0
  119. package/src/providers/antigravity-models.ts +54 -16
  120. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  121. package/src/providers/derive.ts +2 -0
  122. package/src/providers/model-rename-fields.ts +3 -0
  123. package/src/providers/new-model-policy.ts +48 -10
  124. package/src/providers/registry/entries-extended.ts +3 -0
  125. package/src/providers/registry/model-ids.ts +2 -0
  126. package/src/providers/registry/model-seeds.ts +5 -0
  127. package/src/providers/registry/types.ts +4 -0
  128. package/src/responses/compaction.ts +19 -0
  129. package/src/router.ts +6 -0
  130. package/src/server/auth-cors.ts +9 -0
  131. package/src/server/chat-completions.ts +10 -0
  132. package/src/server/claude-messages.ts +18 -11
  133. package/src/server/droid-reasoning-default.ts +50 -0
  134. package/src/server/github-copilot-responses-repair.ts +3 -6
  135. package/src/server/grok-responses-control-frame.ts +2 -2
  136. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  137. package/src/server/gui-session.ts +33 -11
  138. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  139. package/src/server/index/serve-options.ts +2 -1
  140. package/src/server/management/agent-settings-routes.ts +6 -129
  141. package/src/server/management/config-routes.ts +1 -1
  142. package/src/server/management/integration-routes.ts +64 -6
  143. package/src/server/management/link-routes.ts +20 -13
  144. package/src/server/management/model-row-export-metadata.ts +150 -0
  145. package/src/server/management/model-rows.ts +148 -9
  146. package/src/server/management/route-registry.ts +4 -3
  147. package/src/server/management/shared.ts +19 -5
  148. package/src/server/management/subagent-model-routes.ts +214 -0
  149. package/src/server/relay.ts +14 -44
  150. package/src/server/request-log-generation-window.ts +38 -0
  151. package/src/server/request-log.ts +13 -0
  152. package/src/server/responses/adapter-dispatch.ts +100 -1
  153. package/src/server/responses/codex-auth-error.ts +10 -2
  154. package/src/server/responses/combo-stream-preflight.ts +29 -3
  155. package/src/server/responses/compaction-routing.ts +10 -1
  156. package/src/server/responses/core-auth.ts +57 -26
  157. package/src/server/responses/core-codex-account.ts +6 -3
  158. package/src/server/responses/core-combo-failure.ts +5 -1
  159. package/src/server/responses/core-combo-native.ts +7 -1
  160. package/src/server/responses/core-combo.ts +18 -1
  161. package/src/server/responses/core-normalize.ts +7 -0
  162. package/src/server/responses/core-options.ts +5 -0
  163. package/src/server/responses/fetch-helpers.ts +9 -0
  164. package/src/server/responses/passthrough-delivery.ts +1 -1
  165. package/src/server/responses/policy-fallback.ts +1 -1
  166. package/src/server/responses/request-prepare.ts +5 -0
  167. package/src/server/responses/request-spend.ts +30 -30
  168. package/src/server/responses-custom-tool-repair.ts +2 -2
  169. package/src/server/responses-hosted-image-display.ts +70 -9
  170. package/src/server/responses-terminal-repair.ts +15 -2
  171. package/src/server/sse-frame-buffer.ts +38 -36
  172. package/src/server/sse-payload-rewrite.ts +78 -49
  173. package/src/server/ws-bridge.ts +1 -11
  174. package/src/service/windows-taskxml.ts +1 -2
  175. package/src/service-manager-probe.ts +36 -2
  176. package/src/types/config.ts +4 -0
  177. package/src/types/provider.ts +18 -0
  178. package/src/update/npm-invocation.mjs +35 -3
  179. package/src/update/proxy-liveness-probe.mjs +38 -8
  180. package/src/usage/expected-prices.ts +9 -0
  181. package/src/usage/log.ts +7 -0
  182. package/src/usage/summary.ts +82 -1
  183. package/src/web-search/loop.ts +11 -8
  184. package/src/web-search/run-turn-loop.ts +10 -7
  185. package/gui/dist/assets/App-CddorHdA.js +0 -51
  186. package/gui/dist/assets/App-DMudtISW.css +0 -1
  187. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  188. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  189. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,41 @@
1
+ import { jsonUtf8Bytes } from "../../lib/json-byte-size";
2
+
3
+ export const PICKER_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
4
+ const MAX_FIELD_BYTES = 64 * 1024;
5
+ const MAX_ROW_BYTES = 256 * 1024;
6
+ const MAX_ADDED_BYTES = 2 * 1024 * 1024;
7
+ const MAX_ADDED_ROWS = 4096;
8
+
9
+ /** Inspect references only: no deep copy or serialized string is allocated by preflight. */
10
+ export function pickerRowBytes(row: Record<string, unknown>): number {
11
+ for (const key of Object.keys(row)) {
12
+ jsonUtf8Bytes(key, MAX_FIELD_BYTES);
13
+ if (row[key] !== undefined) jsonUtf8Bytes(row[key], MAX_FIELD_BYTES);
14
+ }
15
+ return jsonUtf8Bytes(row, MAX_ROW_BYTES);
16
+ }
17
+
18
+ /** One budget for the entire response, including duplicate surfaces and the CLI fallback. */
19
+ export class PickerRewriteBudget {
20
+ private outputBytes: number;
21
+ private addedBytes = 0;
22
+ private addedRows = 0;
23
+
24
+ constructor(body: unknown) {
25
+ this.outputBytes = jsonUtf8Bytes(body, PICKER_MAX_OUTPUT_BYTES);
26
+ }
27
+
28
+ reserveBytes(bytes: number): void {
29
+ if (bytes > MAX_ADDED_BYTES - this.addedBytes || bytes > PICKER_MAX_OUTPUT_BYTES - this.outputBytes) {
30
+ throw new RangeError("Picker rewrite byte limit");
31
+ }
32
+ this.addedBytes += bytes;
33
+ this.outputBytes += bytes;
34
+ }
35
+
36
+ reserveRow(row: Record<string, unknown>): void {
37
+ if (++this.addedRows > MAX_ADDED_ROWS) throw new RangeError("Picker rewrite row limit");
38
+ // Include a separator even for an empty list, conservatively reserving one extra byte.
39
+ this.reserveBytes(pickerRowBytes(row) + 1);
40
+ }
41
+ }
@@ -248,6 +248,7 @@ export function anthropicIncompleteOutcome(
248
248
  */
249
249
  export function anthropicFailedStatus(error: Rec, message: string): number {
250
250
  const code = typeof error.code === "string" ? error.code : undefined;
251
+ if (code === "context_length_exceeded") return 400;
251
252
  return code === "translation_buffer_limit"
252
253
  ? 413
253
254
  : typeof error.status === "number"
@@ -726,6 +727,7 @@ export function responsesSseToAnthropicSse(
726
727
  status,
727
728
  message,
728
729
  true,
730
+ code === "context_length_exceeded" ? code : undefined,
729
731
  );
730
732
  break;
731
733
  }
@@ -0,0 +1,53 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { claudeConfigDir } from "./gateway-cache";
5
+ import { commandInvocation } from "../lib/win-exec";
6
+
7
+ export interface SubagentForceStatus {
8
+ targetValid: boolean;
9
+ version: string | null;
10
+ support: "supported" | "unsupported" | "unknown";
11
+ settingsOverride: boolean;
12
+ settingsReadable: boolean;
13
+ }
14
+
15
+ export function subagentForceSupport(version: string | null): SubagentForceStatus["support"] {
16
+ const match = version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
17
+ if (!match) return "unknown";
18
+ const [major, minor, patch] = match.slice(1).map(Number);
19
+ return major! > 2 || (major === 2 && (minor! > 1 || (minor === 1 && patch! >= 257))) ? "supported" : "unsupported";
20
+ }
21
+
22
+ function installedVersion(): Promise<string | null> {
23
+ const invocation = commandInvocation("claude", ["--version"]);
24
+ return new Promise(resolve => {
25
+ execFile(invocation.file, invocation.args, { ...invocation.options, timeout: 1500, maxBuffer: 4096 }, (error, stdout) => {
26
+ resolve(error ? null : /^\s*(\d+\.\d+\.\d+)(?:\s|$)/.exec(String(stdout))?.[1] ?? null);
27
+ });
28
+ });
29
+ }
30
+
31
+ /** Server-local observation only: never returns settings values or modifies settings. */
32
+ export async function inspectSubagentForceStatus(
33
+ targetValid: boolean,
34
+ configDir = claudeConfigDir(),
35
+ versionProbe: () => Promise<string | null> = installedVersion,
36
+ ): Promise<SubagentForceStatus> {
37
+ const version = await versionProbe().catch(() => null);
38
+ let settingsOverride = false;
39
+ let settingsReadable = true;
40
+ try {
41
+ const path = join(configDir, "settings.json");
42
+ const metadata = await stat(path);
43
+ if (!metadata.isFile() || metadata.size > 1_048_576) throw new Error("settings unavailable");
44
+ const parsed = JSON.parse(await readFile(path, "utf8"));
45
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid settings");
46
+ const env = parsed.env;
47
+ settingsOverride = !!env && typeof env === "object" && !Array.isArray(env)
48
+ && (Object.hasOwn(env, "CLAUDE_CODE_SUBAGENT_MODEL") || Object.hasOwn(env, "CLAUDE_CODE_SUBAGENT_MODEL_FORCE"));
49
+ } catch (error) {
50
+ settingsReadable = (error as NodeJS.ErrnoException).code === "ENOENT";
51
+ }
52
+ return { targetValid, version, support: subagentForceSupport(version), settingsOverride, settingsReadable };
53
+ }
@@ -0,0 +1,93 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { claudeCodeAlias, claudeCodeNativeAlias, resolveAlias } from "./alias";
3
+ import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
4
+ import { hasOwnProvider } from "../config";
5
+ import { knownModelIdsForProvider } from "../router";
6
+ import { decodeRoutedModelIdOrThrow } from "../providers/slug-codec";
7
+ import { SAFE_AGENT_MODEL_ID } from "../config/subagent-models";
8
+ import { resolveDesktop3pAlias } from "./desktop-3p";
9
+ export { SAFE_AGENT_MODEL_ID } from "../config/subagent-models";
10
+
11
+ /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
12
+ * Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
13
+ * so the alias joins the raw-native context-window map (context-windows.ts). */
14
+
15
+ /**
16
+ * Generated subagent defs cannot rely on the parent's auto-context compaction
17
+ * pairing, so their [1m] marker follows the AUTHORITATIVE window only: mark when
18
+ * the effective window (exact selector, then the canonical [1m] form, then bare)
19
+ * is genuinely >= 1M; strip an inherited unsafe marker back to the bare selector;
20
+ * with no window information, keep the selector as it was. Genuine routed [1m]
21
+ * ids are preserved through the canonical-exact lookup. (#854)
22
+ */
23
+ export function withSubagentContextMarker(selector: string, windows: Record<string, number>): string {
24
+ const bare = stripOneMillionMarker(selector);
25
+ const wasMarked = selector !== bare;
26
+ const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
27
+ const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
28
+ if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
29
+ return shouldMarkOneMillion(authoritativeWindow, AUTO_CONTEXT_OFF)
30
+ ? (withOneMillionMarker(selector, windows) ?? selector)
31
+ : bare;
32
+ }
33
+ return wasMarked ? selector : bare;
34
+ }
35
+ export function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
36
+ const slash = entry.indexOf("/");
37
+ if (slash > 0) {
38
+ const provider = entry.slice(0, slash);
39
+ const prov = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
40
+ const id = prov
41
+ ? decodeRoutedModelIdOrThrow(entry.slice(slash + 1), knownModelIdsForProvider(provider, prov, config))
42
+ : entry.slice(slash + 1);
43
+ return { alias: claudeCodeAlias(provider, id), id, provider };
44
+ }
45
+ return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
46
+ }
47
+
48
+ export type SubagentForceExposure = { entries: readonly string[] } | { selectors: readonly string[] };
49
+
50
+ function fullSelectorIdentity(selector: string): string {
51
+ const route = resolveAlias(selector) ?? resolveDesktop3pAlias(selector) ?? selector;
52
+ return (route.startsWith("native/") ? route.slice("native/".length) : route).replace(/\[1m\]$/i, "[1m]");
53
+ }
54
+
55
+ function selectorIdentity(selector: string): string {
56
+ return fullSelectorIdentity(stripOneMillionMarker(selector));
57
+ }
58
+
59
+ /** Resolve only currently exposed entries; retained roster rows are not exposure proof. */
60
+ export function resolveSubagentForceModel(config: OcxConfig, windows: Record<string, number>, available: SubagentForceExposure): string | null {
61
+ const entry = config.claudeCode?.subagentModelForce;
62
+ if (typeof entry !== "string" || !SAFE_AGENT_MODEL_ID.test(entry)) return null;
63
+ try {
64
+ const parts = entryParts(entry, config);
65
+ if ((config.disabledModels ?? []).some(disabled => {
66
+ try { return selectorIdentity(entryParts(disabled, config).alias) === selectorIdentity(parts.alias); } catch { return false; }
67
+ })) return null;
68
+ const selectors = "selectors" in available ? available.selectors : available.entries.flatMap(candidate => {
69
+ try { return [entryParts(candidate, config).alias]; } catch { return []; }
70
+ });
71
+ const exposed = selectors.some(candidate => selectorIdentity(candidate) === selectorIdentity(parts.alias));
72
+ if (!exposed) return null;
73
+ const bare = stripOneMillionMarker(parts.alias);
74
+ const requestedMarker = bare !== parts.alias;
75
+ const exactMarkedExposure = requestedMarker && selectors.some(candidate =>
76
+ fullSelectorIdentity(candidate) === fullSelectorIdentity(parts.alias));
77
+ const window = windows[parts.alias] ?? windows[`${bare}[1m]`] ?? windows[bare];
78
+ // Identity admission must not let an appended context promise ride on an
79
+ // ordinary exposed model. Exact catalog ids such as kimi/k3[1m] remain literal.
80
+ if (requestedMarker && !exactMarkedExposure && !(typeof window === "number" && Number.isFinite(window) && window >= 1_000_000)) return null;
81
+ const marked = exactMarkedExposure ? parts.alias : withSubagentContextMarker(parts.alias, windows);
82
+ // Bare Anthropic names are indistinguishable from the old CLI's fallback.
83
+ // Encode only the force selector; handlers restore native identity before
84
+ // their existing credential/model-map checks. Roster generation is unchanged.
85
+ if (parts.alias.startsWith("claude-") && !resolveAlias(parts.alias) && !resolveDesktop3pAlias(parts.alias)) {
86
+ const native = claudeCodeNativeAlias(stripOneMillionMarker(marked));
87
+ return marked === stripOneMillionMarker(marked) ? native : `${native}[1m]`;
88
+ }
89
+ return marked;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
@@ -27,6 +27,7 @@ export interface AccountRow {
27
27
  masked?: string;
28
28
  active: boolean;
29
29
  needsReauth?: boolean;
30
+ needsReauthReason?: "verify_account";
30
31
  autoSelectable?: boolean;
31
32
  skipReason?: "paused" | "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
32
33
  selectionExcludedReason?: "plan_excluded";
@@ -348,6 +349,7 @@ interface OAuthAccountDto {
348
349
  /** Present only for providers that support operator pause (generic OAuth pools). */
349
350
  paused?: boolean;
350
351
  autoSwitchThresholdOverride?: number | null;
352
+ needsReauthReason?: "verify_account";
351
353
  autoSelectable?: boolean;
352
354
  skipReason?: unknown;
353
355
  /** Always sent by the management route; explicitly `null` when the tier is unknown. */
@@ -390,6 +392,7 @@ async function fetchOAuthRows(
390
392
  needsReauth: a.needsReauth,
391
393
  ...(a.paused === true ? { paused: true } : {}),
392
394
  ...(name === "anthropic" && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
395
+ ...(a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
393
396
  ...(name === "kiro" && typeof a.autoSelectable === "boolean"
394
397
  ? { autoSelectable: a.autoSelectable } : {}),
395
398
  ...(name === "kiro" && a.autoSelectable === false && isKiroSkipReason(a.skipReason)
@@ -110,7 +110,9 @@ function statusText(row: AccountRow): string {
110
110
  // held out -- so printing only one of the two would hide exactly the confusing case (#2703).
111
111
  if (row.paused) parts.push("paused");
112
112
  if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
113
- if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) parts.push("needs-reauth");
113
+ if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) {
114
+ parts.push(row.needsReauthReason === "verify_account" ? "needs-reauth(verify)" : "needs-reauth");
115
+ }
114
116
  // A paused Kiro row already says "paused"; repeating it as a skip reason adds nothing.
115
117
  if (row.provider === "kiro" && row.autoSelectable === false && !(row.paused && row.skipReason === "paused"))
116
118
  parts.push(row.skipReason ? `not-auto-selected(${row.skipReason})` : "not-auto-selected");
package/src/cli/agent.ts CHANGED
@@ -28,6 +28,7 @@ const USAGE = `Usage:
28
28
  ocx agent injection suggest <work description> [--model <id>] [--apply] [--json]
29
29
  ocx agent effort <status|set> [--main <level|->] [--subagent <level|->] [--json]
30
30
  ocx agent subagents <status|set|clear> [model,model...] [--json]
31
+ ocx agent subagents force <model|-> [--json]
31
32
  ocx agent fallback <status|set|clear> [model,model...] [--poll-ms <5000-600000>] [--json]
32
33
  ocx agent roles [status|set <role> <model>|suggest [--model <id>] [--apply]] [--json]
33
34
  ocx agent sidecar <status|web|vision> [--list] [--model <id|->]
@@ -169,6 +170,15 @@ async function subagents(argv: string[], deps: RuntimeApiDeps): Promise<void> {
169
170
  printData(result, wantsJson, summaryLines(result));
170
171
  return;
171
172
  }
173
+ if (action === "force") {
174
+ const model = args.shift();
175
+ if (!model?.trim()) throw new CliUsageError("a force model or - is required", USAGE);
176
+ rejectArgs(args, USAGE);
177
+ const force = clearable(model);
178
+ const result = await runtimeRequest("/api/subagent-models", { method: "PUT", body: JSON.stringify({ force }) }, deps);
179
+ printData(result, wantsJson, [force ? `Claude subagent force: ${force}. Applies on the next routed ocx claude launch.` : "Claude subagent force cleared."]);
180
+ return;
181
+ }
172
182
  let models: string[];
173
183
  if (action === "clear") models = [];
174
184
  else if (action === "set") {
@@ -1063,6 +1063,15 @@ export const CAPABILITIES: readonly Capability[] = [
1063
1063
  json: "none",
1064
1064
  details: ["The Aside refresh uses the live server; other catalog synchronization also performs local work."],
1065
1065
  },
1066
+ {
1067
+ command: ["agent", "subagents", "force"],
1068
+ summary: "Force Claude Code subagents onto one exposed model at the next routed launch; - clears.",
1069
+ routes: [{ method: "GET", path: "/api/subagent-models" }, { method: "PUT", path: "/api/subagent-models" }],
1070
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the saved force setting." }],
1071
+ mutates: true,
1072
+ json: "payload",
1073
+ details: ["Requires Claude Code 2.1.257+. Plain claude, forks, inherit skills, the main model and small-fast sidecars are unaffected."],
1074
+ },
1066
1075
  {
1067
1076
  command: ["agent", "request-user-input"],
1068
1077
  summary: "Show or set whether default mode may ask the operator a question mid-task.",
@@ -101,23 +101,26 @@ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
101
101
  console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
102
102
  return 1;
103
103
  }
104
+ let binary: string | undefined;
104
105
  if (sub === "launch") {
105
106
  if (config.chatgptDesktop?.appServerShim !== true) {
106
107
  console.error('Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.');
107
108
  return 1;
108
109
  }
109
- const binary = resolveChatgptCodexBinary(install.root);
110
+ binary = resolveChatgptCodexBinary(install.root) ?? undefined;
110
111
  if (!binary) {
111
112
  console.error(`No bundled app-server binary was found in ${install.root}; the shim cannot launch this build.`);
112
113
  return 1;
113
114
  }
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
115
  }
116
+ // Both relaunch paths execute the discovered bundle. Restore validates the app
117
+ // shell without requiring an app-server binary or the experimental opt-in flag.
118
+ const untrusted = untrustedChatgptBundleReason(install.root, binary);
119
+ if (untrusted) {
120
+ console.error(`Refusing to ${sub === "launch" ? "launch the shim" : "restore ChatGPT"}: ${untrusted}.`);
121
+ return 1;
122
+ }
123
+ if (binary) writeChatgptShimLauncher(undefined, binary);
121
124
  if (!(await quitApp(install, launcher))) {
122
125
  console.error("ChatGPT did not quit; quit it manually and retry.");
123
126
  return 1;
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, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
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 = typeof route === "number"
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
  }
@@ -3,6 +3,8 @@ import {
3
3
  getCodexRoutingKind,
4
4
  type CodexRoutingKind,
5
5
  } from "../codex/inject";
6
+ import { diagnoseCodexShim, type CodexShimDiagnostic } from "../codex/shim";
7
+ import { findFirstCodexOnPath, type CodexPathCandidate } from "../codex/shim-path-resolution";
6
8
  import { loadConfig, resolveEnvValue } from "../config";
7
9
 
8
10
  const PROXY_ENV_KEYS = [
@@ -21,6 +23,93 @@ export interface CodexShimReadinessInputs {
21
23
  configuredProxyResolved: boolean;
22
24
  }
23
25
 
26
+ export type CodexConnectShimStatus = "ready" | "missing" | "unhealthy" | "unverified";
27
+
28
+ export interface CodexConnectShimReadiness {
29
+ status: CodexConnectShimStatus;
30
+ /** Secret-free, actionable text rendered by ocx connect. */
31
+ message: string;
32
+ }
33
+
34
+ export interface CodexConnectShimInspectionDeps {
35
+ diagnose?: () => Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
36
+ findOnPath?: () => CodexPathCandidate | null;
37
+ }
38
+
39
+ const CODEX_TOKEN_NOTE = "The connected Codex config uses OPENCODEX_API_AUTH_TOKEN; "
40
+ + "without a working shim or when a PATH wrapper replaces it, Codex may fail with "
41
+ + '"Missing environment variable".';
42
+ const CODEX_TOKEN_ACTION = `${CODEX_TOKEN_NOTE} Run 'ocx codex-shim install' to repair it.`;
43
+
44
+ export function codexConnectShimReadiness(inputs: {
45
+ diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
46
+ commandPath: string | null;
47
+ commandIsShim?: boolean;
48
+ }): CodexConnectShimReadiness {
49
+ if (inputs.diagnosis.installed && !inputs.diagnosis.healthy) {
50
+ return {
51
+ status: "unhealthy",
52
+ message: `installed but unhealthy: ${inputs.diagnosis.summary}. ${CODEX_TOKEN_ACTION}`,
53
+ };
54
+ }
55
+ if (inputs.diagnosis.healthy && inputs.commandPath && !inputs.commandIsShim) {
56
+ // The shim is installed and healthy, so reinstalling it cannot fix this: only PATH
57
+ // order (or the wrapper in front of it) can.
58
+ return {
59
+ status: "missing",
60
+ message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. `
61
+ + `Shim state: ${inputs.diagnosis.summary}. Put the directory of the tracked shim ahead of that command on PATH, `
62
+ + `or remove that wrapper; re-running 'ocx codex-shim install' will not change PATH order. ${CODEX_TOKEN_NOTE}`,
63
+ };
64
+ }
65
+ if (inputs.diagnosis.installed && inputs.diagnosis.healthy) {
66
+ if (!inputs.commandPath) {
67
+ return {
68
+ status: "missing",
69
+ message: `installed but not active; no 'codex' executable was found on PATH. `
70
+ + `Shim state: ${inputs.diagnosis.summary}. Add the directory of the tracked shim to PATH. ${CODEX_TOKEN_NOTE}`,
71
+ };
72
+ }
73
+ return { status: "ready", message: "installed and healthy" };
74
+ }
75
+ if (inputs.commandPath) {
76
+ return {
77
+ status: "missing",
78
+ message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. ${CODEX_TOKEN_ACTION}`,
79
+ };
80
+ }
81
+ return {
82
+ status: "missing",
83
+ message: `not installed and no 'codex' executable was found on PATH. ${CODEX_TOKEN_ACTION}`,
84
+ };
85
+ }
86
+
87
+ export function inspectCodexShimForConnect(
88
+ deps: CodexConnectShimInspectionDeps = {},
89
+ ): CodexConnectShimReadiness {
90
+ let diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
91
+ try {
92
+ diagnosis = (deps.diagnose ?? diagnoseCodexShim)();
93
+ } catch {
94
+ diagnosis = { installed: true, healthy: false, summary: "diagnostic state could not be read" };
95
+ }
96
+ let command: CodexPathCandidate | null = null;
97
+ try {
98
+ command = (deps.findOnPath ?? findFirstCodexOnPath)();
99
+ } catch {
100
+ return {
101
+ status: "unverified",
102
+ message: `PATH activation could not be verified. Shim state: ${diagnosis.summary}. `
103
+ + `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
104
+ };
105
+ }
106
+ return codexConnectShimReadiness({
107
+ diagnosis,
108
+ commandPath: command?.path ?? null,
109
+ commandIsShim: command?.isShim ?? false,
110
+ });
111
+ }
112
+
24
113
  function externalProviderLabel(provider: string | null): string {
25
114
  return provider
26
115
  ? `external model_provider ${JSON.stringify(provider)}`
@@ -40,6 +40,10 @@ import {
40
40
  terminalSafeText,
41
41
  type RuntimeApiDeps,
42
42
  } from "./runtime-api";
43
+ import {
44
+ inspectCodexShimForConnect,
45
+ type CodexConnectShimReadiness,
46
+ } from "./codex-shim-readiness";
43
47
 
44
48
  export interface ClientCommandDeps extends RuntimeApiDeps {
45
49
  lifecycleLockDeps?: ClientLifecycleLockDeps;
@@ -277,6 +281,19 @@ export type ConnectCompletionReport = {
277
281
  readonly failure: string | null;
278
282
  };
279
283
 
284
+ export function codexShimReadinessLine(
285
+ selectedClients: readonly OcxConnectedClientId[],
286
+ inspect: () => CodexConnectShimReadiness = inspectCodexShimForConnect,
287
+ ): string | null {
288
+ if (!selectedClients.includes("codex")) return null;
289
+ return `Codex autostart shim: ${terminalSafeText(inspect().message)}`;
290
+ }
291
+
292
+ function printCodexShimReadiness(selectedClients: readonly OcxConnectedClientId[]): void {
293
+ const line = codexShimReadinessLine(selectedClients);
294
+ if (line) console.log(line);
295
+ }
296
+
280
297
  /**
281
298
  * What `ocx connect` says once the hub and the credential are settled, and whether the command
282
299
  * still fails (#4207).
@@ -414,6 +431,7 @@ async function runHubConnect(argv: string[], deps: ClientCommandDeps): Promise<v
414
431
  const readiness = inspectInstalledCatalogReadiness(installedCatalogFileState(), deps.catalogProbeDeps ?? {});
415
432
  const report = connectCompletionReport(connection, clients, readiness);
416
433
  for (const line of report.lines) console.log(line);
434
+ printCodexShimReadiness(clients);
417
435
  if (report.failure) throw new Error(report.failure);
418
436
  }
419
437
 
@@ -463,6 +481,7 @@ async function runLinkConnect(argv: string[], deps: ClientCommandDeps): Promise<
463
481
  const readiness = inspectInstalledCatalogReadiness(installedCatalogFileState(), deps.catalogProbeDeps ?? {});
464
482
  const report = connectCompletionReport(connection, clients, readiness);
465
483
  for (const line of report.lines) console.log(line);
484
+ printCodexShimReadiness(clients);
466
485
  if (report.failure) throw new Error(report.failure);
467
486
  }
468
487
 
@@ -17,6 +17,7 @@ import type { LivenessIo, LiveProxy } from "../server/proxy-liveness";
17
17
  import type { OcxConfig } from "../types";
18
18
  import type { OwnedIntegrationRefreshOutcome } from "../integrations/owned-refresh";
19
19
  import { hasHelpFlag, printSubcommandUsage, printUsage } from "./help";
20
+ import { printUnknownCommand } from "./help-recovery";
20
21
  import {
21
22
  HUB_GATED_SKIP_MESSAGE,
22
23
  localClientSkipMessage,
@@ -514,7 +515,7 @@ const commandRunners: Record<string, CommandRunner> = {
514
515
  },
515
516
  config,
516
517
  port: live.port,
517
- }, ["mcode", "pi", "raycast", "omo", "cline", "droid"]));
518
+ }, ["mcode", "pi", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
518
519
  } catch (error) {
519
520
  console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
520
521
  }
@@ -1167,8 +1168,7 @@ export async function dispatchCommand(head: CliHead, deps: CliDispatchDeps): Pro
1167
1168
  }
1168
1169
  const runner = commandRunners[resolveDispatchCommand(command) ?? ""];
1169
1170
  if (!runner) {
1170
- console.error(`Unknown command: ${command}`);
1171
- printUsage();
1171
+ printUnknownCommand(command);
1172
1172
  return 1;
1173
1173
  }
1174
1174
  return await runner(deps);
@@ -66,12 +66,13 @@ export interface ExportCommandDeps extends RuntimeApiDeps {
66
66
  * `opencodeCatalogFromProxyRows` owns the visibility rules (drop `disabled`, drop dupes,
67
67
  * drop native under Codex Direct) — the export core does none of that, so a row filtered
68
68
  * here is the only thing keeping a disabled model out of a client's picker. It also carries
69
- * the effort ladder, so the ladder a client receives comes from the same filtered, deduped
70
- * row as the model itself: a second lookup over the raw rows would let a hidden or disabled
71
- * duplicate donate its ladder to the visible entry.
69
+ * the effort ladder and the effective custom-row projection, so every per-model field a
70
+ * client receives comes from the same filtered, deduped row as the model itself: a second
71
+ * lookup over the raw rows would let a hidden or disabled duplicate donate its ladder to
72
+ * the visible entry.
72
73
  *
73
- * Modalities need no such lookup: `opencodeCatalogFromProxyRows` carries them on the catalog
74
- * entry, so the clients that filter them are handed the same filtered, deduped row.
74
+ * The empty-vs-undefined ladder distinction survives the hop: a row declaring `[]` exports
75
+ * `[]` (explicit "no rungs"), a row declaring nothing exports nothing (unknown).
75
76
  */
76
77
  export function exportModelsFromProxyRows(
77
78
  rows: readonly OpencodeProxyModelRow[],
@@ -87,13 +88,21 @@ export function exportModelsFromProxyRows(
87
88
  if (entry.fastRowAvailable !== undefined) model.fastRowAvailable = entry.fastRowAvailable;
88
89
  if (entry.displayName) model.displayName = entry.displayName;
89
90
  if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
90
- if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
91
- model.reasoningEfforts = [...entry.reasoningEfforts];
92
- }
91
+ // The catalog row's own output limit; dropping it here is what once made every CLI
92
+ // export fall back to the generated-metadata guess the management path never used.
93
+ if (entry.maxTokens !== undefined) model.maxTokens = entry.maxTokens;
94
+ if (entry.maxInputTokens !== undefined) model.maxInputTokens = entry.maxInputTokens;
95
+ if (entry.reasoningEfforts !== undefined) model.reasoningEfforts = [...entry.reasoningEfforts];
93
96
  if (entry.defaultReasoningEffort) model.defaultReasoningEffort = entry.defaultReasoningEffort;
94
97
  if (entry.inputModalities && entry.inputModalities.length > 0) {
95
98
  model.inputModalities = [...entry.inputModalities];
96
99
  }
100
+ // Explicit booleans survive, false included; nothing infers a negative from absence.
101
+ if (entry.supportsTools !== undefined) model.supportsTools = entry.supportsTools;
102
+ if (entry.supportsReasoning !== undefined) model.supportsReasoning = entry.supportsReasoning;
103
+ if (entry.supportsReasoningSummaries !== undefined) {
104
+ model.supportsReasoningSummaries = entry.supportsReasoningSummaries;
105
+ }
97
106
  return model;
98
107
  });
99
108
  }
@@ -194,7 +203,9 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
194
203
  // `--out` is the path that writes the selected client's native format.
195
204
  // Format metadata rides in the human lines below.
196
205
  printData(clientConfig, wantsJson, [
197
- text.trimEnd(),
206
+ // `lines` entries print one console line each and are control-escaped, so
207
+ // the document goes in as individual lines rather than one multi-line blob.
208
+ ...text.trimEnd().split("\n"),
198
209
  "",
199
210
  ...(out !== undefined ? [`Wrote ${out}`] : []),
200
211
  `Destination: ${spec.destination(process.env)}`,