@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -0,0 +1,140 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadConfig } from "../config";
5
+ import {
6
+ chatgptShimLauncherPath,
7
+ resolveChatgptCodexBinary,
8
+ writeChatgptShimLauncher,
9
+ } from "../chatgpt/app-server-shim/launcher";
10
+ import { untrustedChatgptBundleReason } from "../chatgpt/app-server-shim/bundle-trust";
11
+ import { darwinDefaultExec, darwinDesktopAppAdapter } from "../codex/desktop-app/darwin";
12
+ import type { DesktopAppInstall } from "../codex/desktop-app/types";
13
+
14
+ const USAGE = `Usage (experimental, macOS only):
15
+ ocx chatgpt launch Relaunch ChatGPT with the experimental app-server shim (requires appServerShim: true)
16
+ ocx chatgpt restore Relaunch ChatGPT without the experimental shim and remove its launcher
17
+ ocx chatgpt status Inspect experimental flag, launcher and running app environment`;
18
+
19
+ function run(command: string, args: string[]) {
20
+ const result = spawnSync(command, args, { encoding: "utf8", timeout: 5000 });
21
+ return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
22
+ }
23
+
24
+ /**
25
+ * The installed app, found and confirmed by bundle identifier (com.openai.codex) the same way the
26
+ * desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
27
+ * relaunching and the binary path all key on this verified bundle rather than on the name.
28
+ */
29
+ function discoverApp(): DesktopAppInstall | null {
30
+ try {
31
+ return darwinDesktopAppAdapter.discover(darwinDefaultExec);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Only inspect the verified bundle's own process; never print the environment being inspected.
39
+ * `-a` keeps ancestors in the match: when ocx runs inside a ChatGPT/Codex session the app is
40
+ * one of this process's ancestors, and plain `pgrep -x` would report it as not running.
41
+ */
42
+ function appState(install: DesktopAppInstall, launcher: string): { running: boolean; shim: boolean } {
43
+ const shell = join(install.root, "Contents", "MacOS", "ChatGPT");
44
+ // Only this user's processes: another account's ChatGPT can neither be quit nor relaunched here.
45
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
46
+ const pids = run("pgrep", [...(uid === undefined ? [] : ["-U", String(uid)]), "-a", "-x", "ChatGPT"]);
47
+ if (!pids.ok) return { running: false, shim: false };
48
+ for (const pid of pids.output.split(/\s+/).filter(value => /^\d+$/.test(value))) {
49
+ const command = run("ps", ["eww", "-o", "command=", "-p", pid]);
50
+ if (command.ok && (command.output === shell || command.output.startsWith(`${shell} `))) {
51
+ const marker = `CODEX_CLI_PATH=${launcher}`;
52
+ const start = command.output.indexOf(marker);
53
+ return { running: true, shim: start >= 0 && (start === 0 || command.output[start - 1] === " ")
54
+ && (start + marker.length === command.output.length || command.output[start + marker.length] === " ") };
55
+ }
56
+ }
57
+ return { running: false, shim: false };
58
+ }
59
+
60
+ /** Adapted from #5947: open ignores new launch settings until the previous app exits. */
61
+ async function quitApp(install: DesktopAppInstall, launcher: string): Promise<boolean> {
62
+ if (!appState(install, launcher).running) return true;
63
+ for (let attempt = 0; attempt < 3; attempt++) {
64
+ run("/usr/bin/osascript", ["-e", `quit app id "${install.id}"`]);
65
+ for (let poll = 0; poll < 20; poll++) {
66
+ if (!appState(install, launcher).running) return true;
67
+ await Bun.sleep(250);
68
+ }
69
+ }
70
+ return !appState(install, launcher).running;
71
+ }
72
+
73
+ export async function handleChatgptCommand(args: string[], platform: NodeJS.Platform = process.platform): Promise<number> {
74
+ const sub = args[0];
75
+ if (!sub || ["help", "--help", "-h"].includes(sub)) {
76
+ console.log(USAGE);
77
+ return sub ? 0 : 64;
78
+ }
79
+ if (!["launch", "restore", "status"].includes(sub) || args.length !== 1) {
80
+ console.error(USAGE);
81
+ return 64;
82
+ }
83
+ if (platform !== "darwin") {
84
+ console.error("ChatGPT app-server shim (experimental): macOS only.");
85
+ return 1;
86
+ }
87
+ try {
88
+ const config = loadConfig();
89
+ const launcher = chatgptShimLauncherPath();
90
+ const install = discoverApp();
91
+ if (sub === "status") {
92
+ const app = install ? appState(install, launcher) : { running: false, shim: false };
93
+ console.log(`app-server shim (experimental): ${config.chatgptDesktop?.appServerShim === true ? "on" : "off"}
94
+ launcher: ${existsSync(launcher) ? "present" : "absent"}
95
+ app: ${install ? (app.running ? "running" : "not running") : "not installed"}
96
+ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
97
+ return 0;
98
+ }
99
+ if (!install) {
100
+ if (sub === "restore") rmSync(launcher, { force: true });
101
+ console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
102
+ return 1;
103
+ }
104
+ if (sub === "launch") {
105
+ if (config.chatgptDesktop?.appServerShim !== true) {
106
+ console.error('Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.');
107
+ return 1;
108
+ }
109
+ const binary = resolveChatgptCodexBinary(install.root);
110
+ if (!binary) {
111
+ console.error(`No bundled app-server binary was found in ${install.root}; the shim cannot launch this build.`);
112
+ return 1;
113
+ }
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
+ }
121
+ if (!(await quitApp(install, launcher))) {
122
+ console.error("ChatGPT did not quit; quit it manually and retry.");
123
+ return 1;
124
+ }
125
+ // Remove an inherited override too: restore must launch without CODEX_CLI_PATH.
126
+ const env = { ...process.env };
127
+ delete env.CODEX_CLI_PATH;
128
+ // Open the verified bundle by path, so the relaunch is the same app that was quit.
129
+ const result = spawnSync("/usr/bin/open", ["-a", install.root, ...(sub === "launch" ? ["--env", `CODEX_CLI_PATH=${launcher}`] : [])], {
130
+ encoding: "utf8", env, timeout: 10000,
131
+ });
132
+ if (result.status !== 0) throw new Error(result.error?.message ?? (result.stderr?.trim() || "open failed"));
133
+ if (sub === "restore") rmSync(launcher, { force: true });
134
+ console.log(`ChatGPT relaunched ${sub === "launch" ? "with" : "without"} the experimental app-server shim.`);
135
+ return 0;
136
+ } catch (error) {
137
+ console.error(`ChatGPT shim (experimental): ${error instanceof Error ? error.message : String(error)}`);
138
+ return 1;
139
+ }
140
+ }
package/src/cli/combo.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { JEV_DECISION_TIMEOUT_MAX_MS, JEV_DECISION_TIMEOUT_MIN_MS } from "../combos/types";
1
2
  import {
2
3
  CliUsageError,
3
4
  printData,
@@ -5,7 +6,6 @@ import {
5
6
  runCliAction,
6
7
  runtimeRequest,
7
8
  takeFlag,
8
- takeIntegerOption,
9
9
  takeOption,
10
10
  type RuntimeApiDeps,
11
11
  } from "./runtime-api";
@@ -13,15 +13,21 @@ import {
13
13
  const USAGE = `Usage:
14
14
  ocx combo [list] [--json]
15
15
  ocx combo show <id> [--json]
16
- ocx combo set <id> --targets <provider/model[:weight],...>
17
- [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100>]
18
- [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force>]
16
+ ocx combo set <id> [--targets <provider/model[:weight],...>]
17
+ [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->]
18
+ [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->]
19
19
  (force overrides valid client effort and can increase cost/latency) [--alias <name|->]
20
20
  [--native-alias] [--display-name <label|->]
21
+ [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->]
22
+ (jev only; the provider must be a configured jev-decision row)
21
23
  [--rename-from <id>] [--json]
22
- ocx combo remove <id> --yes [--json]`;
24
+ ocx combo remove <id> --yes [--json]
25
+ ocx combo test [--combo <id>] [--decision-provider <provider|jev> | --decision-model <route>]
26
+ [--decision-timeout <ms>] [--json]
27
+ (one synthetic decision probe through the decision method; may spend a decision call)
28
+ ocx combo discover [--query <text>] [--json]`;
23
29
 
24
- type ComboRow = Record<string, unknown> & { id?: string; model?: string };
30
+ type ComboRow = Record<string, unknown> & { id?: string; model?: string; strategy?: string };
25
31
 
26
32
  function parseTargets(value: string): Array<{ provider: string; model: string; weight?: number }> {
27
33
  const targets = value.split(",").map(part => part.trim()).filter(Boolean).map(part => {
@@ -72,35 +78,87 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
72
78
  const wantsJson = takeFlag(args, "--json");
73
79
  if (!id) throw new CliUsageError("combo id is required", USAGE);
74
80
  const targetsRaw = takeOption(args, "--targets");
75
- if (!targetsRaw) throw new CliUsageError("--targets is required", USAGE);
76
- const strategy = takeOption(args, "--strategy") ?? "failover";
81
+ const renameFrom = takeOption(args, "--rename-from");
82
+ const partialCurrent = targetsRaw === undefined
83
+ ? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps)
84
+ : undefined;
85
+ const partialExisting = partialCurrent?.combos?.find(row => row.id === (renameFrom ?? id));
86
+ if (targetsRaw === undefined && !partialExisting) {
87
+ throw new CliUsageError("--targets is required when creating a combo", USAGE);
88
+ }
89
+ const strategyRaw = takeOption(args, "--strategy");
90
+ const strategy = strategyRaw ?? partialExisting?.strategy ?? "failover";
77
91
  if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window" && strategy !== "jev") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, reset-window, or jev", USAGE);
78
- const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 });
92
+ const stickyRaw = takeOption(args, "--sticky");
93
+ const stickyLimit = stickyRaw === undefined ? undefined : stickyRaw === "-" ? 1 : Number(stickyRaw);
94
+ if (stickyLimit !== undefined && (!Number.isInteger(stickyLimit) || stickyLimit < 1)) {
95
+ throw new CliUsageError("--sticky must be an integer from 1 to 100, or -", USAGE);
96
+ }
79
97
  if (stickyLimit !== undefined) {
80
98
  if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
81
99
  if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
82
100
  }
83
101
  const effort = takeOption(args, "--effort");
84
102
  const effortMode = takeOption(args, "--effort-mode");
85
- if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") {
86
- throw new CliUsageError("--effort-mode must be fallback or force", USAGE);
103
+ if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force" && effortMode !== "-") {
104
+ throw new CliUsageError("--effort-mode must be fallback, force, or -", USAGE);
87
105
  }
88
106
  const alias = takeOption(args, "--alias");
89
107
  const nativeAlias = takeFlag(args, "--native-alias");
90
108
  const displayName = takeOption(args, "--display-name");
91
- const renameFrom = takeOption(args, "--rename-from");
109
+ const decisionProvider = takeOption(args, "--decision-provider");
110
+ const decisionModel = takeOption(args, "--decision-model");
111
+ if (decisionModel !== undefined && decisionModel !== "-" && decisionProvider !== undefined && decisionProvider !== "-") {
112
+ throw new CliUsageError("--decision-model cannot be combined with --decision-provider", USAGE);
113
+ }
114
+ if (decisionModel !== undefined && decisionModel !== "-" && strategy !== "jev") {
115
+ throw new CliUsageError("--decision-model applies only to the jev strategy", USAGE);
116
+ }
117
+ if (decisionProvider !== undefined && decisionProvider !== "-" && strategy !== "jev") {
118
+ throw new CliUsageError("--decision-provider applies only to the jev strategy", USAGE);
119
+ }
120
+ const decisionTimeout = takeOption(args, "--decision-timeout");
121
+ let decisionTimeoutMs: number | null | undefined;
122
+ if (decisionTimeout !== undefined) {
123
+ decisionTimeoutMs = decisionTimeout === "-" ? null : Number(decisionTimeout);
124
+ if (decisionTimeoutMs !== null
125
+ && (!Number.isInteger(decisionTimeoutMs)
126
+ || decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS
127
+ || decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS)) {
128
+ throw new CliUsageError(
129
+ `--decision-timeout must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}, or -`,
130
+ USAGE,
131
+ );
132
+ }
133
+ if (decisionTimeoutMs !== null && strategy !== "jev") {
134
+ throw new CliUsageError("--decision-timeout applies only to the jev strategy", USAGE);
135
+ }
136
+ }
92
137
  rejectArgs(args, USAGE);
93
- const combo: Record<string, unknown> = {
94
- strategy,
95
- stickyLimit: stickyLimit ?? 1,
96
- targets: parseTargets(targetsRaw),
97
- };
138
+ // Listing-only nulls must not be resent as explicit clears (alias rejects null).
139
+ const combo: Record<string, unknown> = partialExisting
140
+ ? Object.fromEntries(Object.entries(partialExisting).filter(([, value]) => value !== null))
141
+ : { strategy, stickyLimit: stickyLimit ?? 1, targets: parseTargets(targetsRaw!) };
142
+ delete combo.id;
143
+ delete combo.model;
144
+ if (strategyRaw !== undefined) combo.strategy = strategy;
145
+ if (stickyLimit !== undefined) combo.stickyLimit = stickyLimit;
98
146
  if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
99
- if (effortMode !== undefined) combo.defaultEffortMode = effortMode;
147
+ if (effortMode !== undefined) combo.defaultEffortMode = effortMode === "-" ? "fallback" : effortMode;
100
148
  if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
101
149
  if (nativeAlias) combo.nativeAlias = true;
102
150
  if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
103
- const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
151
+ if (decisionProvider !== undefined) combo.decisionProvider = decisionProvider === "-" ? null : decisionProvider;
152
+ if (decisionModel !== undefined) combo.decisionModel = decisionModel === "-" ? null : decisionModel;
153
+ if (decisionProvider !== undefined && decisionProvider !== "-") combo.decisionModel = null;
154
+ if (decisionModel !== undefined && decisionModel !== "-") combo.decisionProvider = null;
155
+ if (decisionTimeoutMs !== undefined) combo.decisionTimeoutMs = decisionTimeoutMs;
156
+ if (strategy !== "jev") {
157
+ delete combo.decisionProvider;
158
+ delete combo.decisionModel;
159
+ delete combo.decisionTimeoutMs;
160
+ }
161
+ const current = partialCurrent ?? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
104
162
  const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
105
163
  if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
106
164
  if (effortMode === undefined && existing?.defaultEffortMode === "force") {
@@ -125,6 +183,71 @@ async function remove(argv: string[], deps: RuntimeApiDeps): Promise<void> {
125
183
  printData(result, wantsJson, [`Removed combo ${id}.`]);
126
184
  }
127
185
 
186
+ async function testDecision(argv: string[], deps: RuntimeApiDeps): Promise<void> {
187
+ const args = [...argv];
188
+ const wantsJson = takeFlag(args, "--json");
189
+ const comboId = takeOption(args, "--combo")?.trim();
190
+ let decisionProvider = takeOption(args, "--decision-provider")?.trim();
191
+ let decisionModel = takeOption(args, "--decision-model")?.trim();
192
+ const timeoutRaw = takeOption(args, "--decision-timeout");
193
+ rejectArgs(args, USAGE);
194
+ if (decisionProvider && decisionModel) {
195
+ throw new CliUsageError("use either --decision-provider or --decision-model", USAGE);
196
+ }
197
+ let decisionTimeoutMs: number | undefined = timeoutRaw === undefined ? undefined : Number(timeoutRaw);
198
+ if (decisionTimeoutMs !== undefined && (!Number.isInteger(decisionTimeoutMs)
199
+ || decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS || decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS)) {
200
+ throw new CliUsageError(
201
+ `--decision-timeout must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}`,
202
+ USAGE,
203
+ );
204
+ }
205
+ if (comboId && !decisionProvider && !decisionModel) {
206
+ // Without an override, probe the decision method the combo has saved.
207
+ const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
208
+ const row = (current.combos ?? []).find(item => item.id === comboId);
209
+ if (!row) throw new CliUsageError(`unknown combo ${comboId}`, USAGE);
210
+ if (row.strategy !== "jev") throw new CliUsageError(`combo ${comboId} does not use the jev strategy`, USAGE);
211
+ if (typeof row.decisionModel === "string") decisionModel = row.decisionModel;
212
+ else if (typeof row.decisionProvider === "string") decisionProvider = row.decisionProvider;
213
+ if (decisionTimeoutMs === undefined && typeof row.decisionTimeoutMs === "number") decisionTimeoutMs = row.decisionTimeoutMs;
214
+ }
215
+ const result = await runtimeRequest<Record<string, unknown>>("/api/combos/decision-test", {
216
+ method: "POST",
217
+ body: JSON.stringify({
218
+ ...(comboId ? { comboId } : {}),
219
+ ...(decisionProvider ? { decisionProvider } : {}),
220
+ ...(decisionModel ? { decisionModel } : {}),
221
+ ...(decisionTimeoutMs !== undefined ? { decisionTimeoutMs } : {}),
222
+ }),
223
+ }, deps);
224
+ const outcome = result.ok === true ? "Decision probe applied" : "Decision probe failed open";
225
+ printData(result, wantsJson, [
226
+ `${outcome} (${String(result.backend)}, gate ${String(result.gate)}, ${String(result.latencyMs)} ms).`,
227
+ ]);
228
+ }
229
+
230
+ async function discover(argv: string[], deps: RuntimeApiDeps): Promise<void> {
231
+ const args = [...argv];
232
+ const wantsJson = takeFlag(args, "--json");
233
+ const query = takeOption(args, "--query")?.trim();
234
+ rejectArgs(args, USAGE);
235
+ const path = query ? `/api/combos/decision-discovery?q=${encodeURIComponent(query)}` : "/api/combos/decision-discovery";
236
+ const result = await runtimeRequest<{
237
+ configured?: Array<{ id: string; url: string; model?: string; usable: boolean; issue?: string }>;
238
+ discovered?: Array<{ provider: string; model: string; endpoint: string }>;
239
+ }>(path, {}, deps);
240
+ const lines = [
241
+ ...(result.configured ?? []).map(row => {
242
+ const model = row.model ? ` (${row.model})` : "";
243
+ const issue = row.usable ? "" : ` [${row.issue ?? "unusable"}]`;
244
+ return `row ${row.id}: ${row.url}${model}${issue}`;
245
+ }),
246
+ ...(result.discovered ?? []).map(row => `catalog ${row.provider}/${row.model}: ${row.endpoint}`),
247
+ ];
248
+ printData(result, wantsJson, lines.length ? lines : ["No decision services found."]);
249
+ }
250
+
128
251
  export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
129
252
  return runCliAction(async () => {
130
253
  const [sub = "list", ...rest] = argv;
@@ -132,6 +255,8 @@ export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps =
132
255
  else if (sub === "show") await show(rest, deps);
133
256
  else if (sub === "set" || sub === "create" || sub === "update") await set(rest, deps);
134
257
  else if (sub === "remove" || sub === "delete") await remove(rest, deps);
258
+ else if (sub === "test") await testDecision(rest, deps);
259
+ else if (sub === "discover") await discover(rest, deps);
135
260
  else throw new CliUsageError(`unknown combo command ${sub}`, USAGE);
136
261
  });
137
262
  }
@@ -159,14 +159,16 @@ function setPath(root: Record<string, unknown>, path: string, value: unknown, re
159
159
  else current[leaf] = value;
160
160
  }
161
161
 
162
+ /** Interpret config-set input as JSON when valid, otherwise preserve it as a string. */
162
163
  function parseValue(raw: string): unknown {
163
164
  try { return JSON.parse(raw); }
164
165
  catch { return raw; }
165
166
  }
166
167
 
168
+ /** Accept one UTF-8 BOM from Windows JSON files or piped input. */
167
169
  function loadInput(path: string): unknown {
168
170
  const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
169
- try { return JSON.parse(raw); }
171
+ try { return JSON.parse(raw.replace(/^\uFEFF/, "")); }
170
172
  catch { throw new CliUsageError(`invalid JSON in ${path}`); }
171
173
  }
172
174
 
@@ -351,7 +351,8 @@ const commandRunners: Record<string, CommandRunner> = {
351
351
  console.error(clientState.kind === "connected"
352
352
  ? "Client mode does not start a local provider proxy; use 'ocx sync'."
353
353
  : `Client state is ${clientState.kind}: ${clientState.reason}`);
354
- return 1;
354
+ // A validated client delegates inference to its hub; no local startup is needed.
355
+ return clientState.kind === "connected" ? 0 : 1;
355
356
  }
356
357
  await deps.handleEnsure();
357
358
  return Number(process.exitCode ?? 0);
@@ -947,6 +948,10 @@ const commandRunners: Record<string, CommandRunner> = {
947
948
  const { handleLabCommand } = await import("./lab");
948
949
  return await handleLabCommand(deps.args.slice(1));
949
950
  },
951
+ chatgpt: async deps => {
952
+ const { handleChatgptCommand } = await import("./chatgpt-command");
953
+ return await handleChatgptCommand(deps.args.slice(1));
954
+ },
950
955
  claude: async deps => {
951
956
  const { cmdClaude } = await import("./claude");
952
957
  // "ocx claude desktop" → write Desktop 3P config
@@ -956,6 +961,10 @@ const commandRunners: Record<string, CommandRunner> = {
956
961
  if (exitCode !== 0) return exitCode;
957
962
  return 0;
958
963
  }
964
+ if (deps.args[1] === "intercept") {
965
+ const { handleClaudeInterceptCommand } = await import("./integrations");
966
+ return await handleClaudeInterceptCommand(deps.args.slice(2));
967
+ }
959
968
  if (deps.args[1] === "config") {
960
969
  const { handleClaudeConfigCommand } = await import("./integrations");
961
970
  return await handleClaudeConfigCommand(deps.args.slice(2));
package/src/cli/help.ts CHANGED
@@ -90,6 +90,7 @@ Usage:
90
90
  ocx config <sub> Validated configuration show/get/set/import/export
91
91
  ocx companion <show|set|reset> Menu-bar and widget companion usage settings
92
92
  ocx lab <sub> Read-only Compatibility Lab projection inspection
93
+ ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
93
94
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
94
95
  ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
95
96
  ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
package/src/cli/index.ts CHANGED
@@ -127,7 +127,6 @@ import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxySti
127
127
  import { acquireOwnershipMutationLease } from "../service/ownership-mutation-lease.mjs";
128
128
  import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
129
129
  import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
130
- import { buildDesktop3pRegistry } from "../claude/desktop-3p";
131
130
  import { startTokenGuardian } from "../oauth/token-guardian";
132
131
  import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
133
132
  import { maybeShowStarPrompt } from "./star-prompt";
@@ -711,27 +710,9 @@ async function handleStart(options: { block?: boolean } = {}) {
711
710
  ? Promise.resolve(null)
712
711
  : syncClaudeAgentDefsAtProxyStartup(config, port),
713
712
  async () => {
714
- try {
715
- const { fetchAllModels } = await import("../server/management-api");
716
- const { desktopVisibleNativeSlugs } = await import("../codex/catalog");
717
- const { resolveAdmittedCodexModelEntitlements } = await import("../codex/model-entitlement-admission");
718
- const { buildDesktopDiscoveryInputs } = await import("../claude/desktop-discovery-inputs");
719
- const [models, modelEntitlements] = await Promise.all([
720
- fetchAllModels(config),
721
- resolveAdmittedCodexModelEntitlements(config, { clientVersion: null }),
722
- ]);
723
- const inputs = buildDesktopDiscoveryInputs({
724
- config, models, modelEntitlements,
725
- desktopNativeCandidates: desktopVisibleNativeSlugs(config),
726
- });
727
- buildDesktop3pRegistry(
728
- inputs.nativeSlugs, inputs.routedModels,
729
- config.claudeCode?.desktopProfile, inputs.nativeContextCap,
730
- );
731
- } catch {
732
- // Best-effort; model discovery can rebuild it. Never reflect credential or provider errors.
733
- console.warn("[opencodex] Claude Desktop model registry could not be initialized at startup.");
734
- }
713
+ // Shared with the Claude Code CLI picker, which awaits this same build (desktop-3p-startup.ts).
714
+ const { initDesktop3pRegistry } = await import("../claude/desktop-3p-startup");
715
+ await initDesktop3pRegistry(config);
735
716
  },
736
717
  );
737
718
  if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config), server.port ?? port));
@@ -1212,7 +1193,7 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
1212
1193
 
1213
1194
  if (snapshot) {
1214
1195
  if (guardedStep?.effect === "approval-changed") {
1215
- return approvalChanged();
1196
+ return approvalChanged(guardedStep.detail);
1216
1197
  }
1217
1198
  if (guardedStep?.effect === "manager-still-active") {
1218
1199
  return managerStillActive(record.service, record);
@@ -1705,7 +1686,7 @@ async function handleStatus() {
1705
1686
  process.exit(1);
1706
1687
  }
1707
1688
 
1708
- const status = await collectStatus();
1689
+ const status = await collectStatus({ mainAccountPolicy: wantsJson });
1709
1690
  if (wantsJson) {
1710
1691
  console.log(JSON.stringify(status.json, null, 2));
1711
1692
  return;
package/src/cli/init.ts CHANGED
@@ -11,6 +11,14 @@ import { deriveInitProviders } from "../providers/derive";
11
11
  import { pinSponsorsWithinKind } from "../providers/sponsor-order";
12
12
  import type { OcxConfig, OcxProviderConfig } from "../types";
13
13
 
14
+ /** Parse a setup TCP port; only an empty answer selects the default. */
15
+ export function parseInitPort(raw: string): number | null {
16
+ const text = raw.trim();
17
+ if (!text) return 10100;
18
+ const value = /^\d+$/.test(text) ? Number(text) : NaN;
19
+ return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
20
+ }
21
+
14
22
  class InitCancelledError extends Error {
15
23
  constructor(readonly exitCode: 1 | 130) {
16
24
  super(exitCode === 130 ? "Setup cancelled." : "stdin reached EOF while waiting for input. Re-run `ocx init` in an interactive terminal.");
@@ -111,6 +119,7 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
111
119
  } catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
112
120
  }
113
121
 
122
+ /** Create a first configuration interactively, preserving existing files and refusing invalid input. */
114
123
  export async function runInit(): Promise<void> {
115
124
  const initial = observeInitialConfigState();
116
125
  if (initial === "exists") {
@@ -194,8 +203,13 @@ export async function runInit(): Promise<void> {
194
203
  };
195
204
  }
196
205
 
197
- const portStr = await prompt.ask("\nProxy port [10100]: ");
198
- const port = parseInt(portStr, 10) || 10100;
206
+ // A mistyped port re-asks instead of discarding every earlier answer; EOF and SIGINT
207
+ // still reject the pending question with InitCancelledError.
208
+ let port: number | null;
209
+ do {
210
+ port = parseInitPort(await prompt.ask("\nProxy port [10100]: "));
211
+ if (port === null) console.error("Proxy port must be a whole decimal number from 1 to 65535. Please try again.");
212
+ } while (port === null);
199
213
 
200
214
  initializeProviderModelSelection(providerName, providerConfig);
201
215
  const config: OcxConfig = {
@@ -368,3 +368,16 @@ export async function handleZcodeCommand(argv: string[], deps: RuntimeApiDeps =
368
368
  }
369
369
  return code;
370
370
  }
371
+
372
+ export async function handleClaudeInterceptCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
373
+ return runCliAction(async () => {
374
+ const args = [...argv];
375
+ const action = args.shift();
376
+ const wantsJson = takeFlag(args, "--json");
377
+ const usage = "Usage: ocx claude intercept start [--json]";
378
+ rejectArgs(args, usage);
379
+ if (action !== "start") throw new CliUsageError("Expected start", usage);
380
+ const result = await runtimeRequest("/api/claude-intercept/start", { method: "POST" }, deps);
381
+ printData(result, wantsJson, summaryLines(result));
382
+ });
383
+ }
@@ -3,21 +3,20 @@
3
3
  *
4
4
  * Deliberately NOT in `src/cli/registry.ts` and NOT in `src/cli/capabilities.ts`, so it
5
5
  * is absent from help, from the generated skill surface, and from the registry-parity
6
- * gate. It is not a user-facing capability and must not become one: it exists so the
7
- * detached restart helper is the same audited binary running the same audited ladder,
8
- * rather than a second implementation in a shell script.
6
+ * gate. These are implementation entrypoints for the detached restart helper and
7
+ * experimental ChatGPT stdout filter; compiled builds re-enter the same CLI binary.
9
8
  *
10
9
  * It is routed before the dispatch table for the same reason `help` is - adding it as a
11
10
  * runner key would make it a command the registry-parity test expects to find
12
11
  * documented.
13
12
  *
14
- * It is intentionally unauthenticated, and that is not an oversight. Any process running
13
+ * The restart handoff is intentionally unauthenticated, and that is not an oversight. Any process running
15
14
  * as this user can invoke it with a hand-written plan file, and it gains nothing by
16
15
  * doing so: the helper only does what the public `--restart-codex` flag already does for
17
16
  * that same user, and a same-uid process could call `kill` directly. A token here would
18
17
  * protect nothing and would imply a boundary that does not exist.
19
18
  */
20
- const USAGE = "Usage: ocx internal desktop-restart-handoff --plan <path>";
19
+ const USAGE = "Usage: ocx internal desktop-restart-handoff --plan <path> | chatgpt-app-server-filter [--self-test]";
21
20
 
22
21
  function optionValue(args: readonly string[], name: string): string | undefined {
23
22
  const index = args.indexOf(name);
@@ -26,6 +25,14 @@ function optionValue(args: readonly string[], name: string): string | undefined
26
25
 
27
26
  export async function handleInternalCommand(args: readonly string[]): Promise<number> {
28
27
  const sub = args[0];
28
+ if (sub === "chatgpt-app-server-filter") {
29
+ if (args.length > 2 || (args[1] !== undefined && args[1] !== "--self-test")) {
30
+ console.error(USAGE);
31
+ return 2;
32
+ }
33
+ const { runChatgptAppServerFilter } = await import("../chatgpt/app-server-shim/filter");
34
+ return await runChatgptAppServerFilter({ selfTest: args[1] === "--self-test" });
35
+ }
29
36
  if (sub !== "desktop-restart-handoff") {
30
37
  console.error(`Unknown internal command: ${sub ?? "(none)"}. ${USAGE}`);
31
38
  return 2;
@@ -52,6 +52,7 @@ function logRows(data: unknown): LogEntry[] {
52
52
  return [];
53
53
  }
54
54
 
55
+ /** Render one human log row with an exact, control-free request ID suitable for history lookup. */
55
56
  function formatLog(row: LogEntry): string {
56
57
  const time = String(row.timestamp ?? row.createdAt ?? "");
57
58
  const route = [row.provider, row.model].filter(Boolean).join("/");
@@ -69,9 +70,13 @@ function formatLog(row: LogEntry): string {
69
70
  const account = typeof row.accountLogLabel === "string" && row.accountLogLabel.length > 0
70
71
  ? `acct=${row.accountLogLabel}`
71
72
  : "";
72
- return [time, String(status), route, duration, account, conversation].filter(Boolean).join(" ");
73
+ const requestId = typeof row.requestId === "string" && row.requestId.length > 0
74
+ && !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(row.requestId)
75
+ ? `id=${row.requestId}` : "";
76
+ return [time, String(status), route, duration, account, conversation, requestId].filter(Boolean).join(" ");
73
77
  }
74
78
 
79
+ /** Read or follow request logs while preserving raw JSON and JSONL output. */
75
80
  async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
76
81
  const args = [...argv];
77
82
  const wantsJson = takeFlag(args, "--json");
@@ -8,6 +8,12 @@ export interface CliCommandEntry {
8
8
  }
9
9
 
10
10
  export const CLI_COMMANDS: CliCommandEntry[] = [
11
+ {
12
+ name: "chatgpt",
13
+ usage: "ocx chatgpt <launch|restore|status>",
14
+ summary: "Experimental ChatGPT app-server shim (macOS only, default off).",
15
+ details: ["Experimental launch requires chatgptDesktop.appServerShim: true; restore removes its launcher."],
16
+ },
11
17
  {
12
18
  name: "init",
13
19
  aliases: ["setup"],
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CliUsageError,
3
+ RuntimeApiError,
3
4
  printData,
4
5
  rejectArgs,
5
6
  runCliAction,
@@ -38,6 +39,7 @@ async function list(argv: string[], deps: RuntimeApiDeps): Promise<void> {
38
39
  );
39
40
  }
40
41
 
42
+ /** Show an existing routing profile, distinguishing malformed usage from a missing record. */
41
43
  async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
42
44
  const args = [...argv];
43
45
  const id = args.shift();
@@ -46,7 +48,7 @@ async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
46
48
  rejectArgs(args, USAGE);
47
49
  const result = await runtimeRequest<{ profiles?: ProfileRow[] }>("/api/routing-profiles", {}, deps);
48
50
  const profile = (result.profiles ?? []).find(candidate => candidate.id === id);
49
- if (!profile) throw new CliUsageError(`unknown routing profile: ${id}`, USAGE);
51
+ if (!profile) throw new RuntimeApiError(`unknown routing profile: ${id}`, 404, null);
50
52
  printData(profile, wantsJson);
51
53
  }
52
54