@bitkyc08/opencodex 2.77.0 → 2.78.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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import { findLiveProxy, probeHostname, type LivenessIo, type LiveProxy } from "../server/proxy-liveness";
13
13
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
14
+ import { redactSecretArgs } from "./secret-args";
15
+ export { redactSecretArgs };
14
16
 
15
17
  export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
16
18
 
@@ -22,6 +24,8 @@ export interface RuntimeApiDeps {
22
24
  /** Test injection for commands that read a secret from stdin instead of argv. */
23
25
  stdinImpl?: CliStdin;
24
26
  stdinTimeoutMs?: number;
27
+ /** Cancellation for bounded secret-byte reads; line readers retain their contract. */
28
+ stdinSignal?: AbortSignal;
25
29
  /** Optional proxy liveness probe injection for commands that check or fall back around live runtime state. */
26
30
  findLiveProxy?: (io?: LivenessIo) => Promise<LiveProxy | null>;
27
31
  }
@@ -33,11 +37,15 @@ export class CliUsageError extends Error {
33
37
  }
34
38
  }
35
39
 
40
+ export const PROXY_NOT_RUNNING_MESSAGE = "Proxy is not running. Start the intended proxy with: ocx start. No request was sent.";
41
+
36
42
  export class RuntimeApiError extends Error {
37
43
  constructor(
38
44
  message: string,
39
45
  readonly status: number,
40
46
  readonly body: unknown,
47
+ /** Local discovery refusals occur before the management request is sent. */
48
+ readonly code?: "proxy_not_running" | "client_role_management_unavailable",
41
49
  ) {
42
50
  super(message);
43
51
  this.name = "RuntimeApiError";
@@ -70,12 +78,12 @@ function clientRoleManagementRefusal(port: number): string {
70
78
  export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
71
79
  if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
72
80
  const live = await (deps.findLiveProxy ?? findLiveProxy)();
73
- if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
81
+ if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
74
82
  // The role comes from the same identity-checked /healthz body liveness already parsed, so
75
83
  // this costs no extra request. Only the client role is refused: an absent role is a
76
84
  // standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
77
85
  if (live.role === "client") {
78
- throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null);
86
+ throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null, "client_role_management_unavailable");
79
87
  }
80
88
  return `http://${probeHostname(live.hostname)}:${live.port}`;
81
89
  }
@@ -85,6 +93,33 @@ function stringField(record: Record<string, unknown>, key: string): string | und
85
93
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
86
94
  }
87
95
 
96
+ function integrationRecoveryMessage(reason: string | undefined): string | undefined {
97
+ switch (reason) {
98
+ case "not_installed": return "The client is not installed. Run: ocx integration client status.";
99
+ case "non_loopback": return "The integration requires a loopback destination. Run: ocx integration client status.";
100
+ case "drift_requires_confirm": return "The client configuration changed. Run `ocx integration client status` before explicitly confirming restore.";
101
+ case "snapshot_expired": return "The recovery snapshot expired. Run `ocx integration client status` before creating a new change.";
102
+ case "superseded_store": return "The client uses a different configuration store. Run `ocx integration client status`, then disable and enable the selected client.";
103
+ case "unsafe": return "The client config path is unsafe to edit (for example a link or a competing file). Run `ocx integration client status` to see the affected path.";
104
+ case "conflict": return "The client config no longer matches the recorded ownership. Run `ocx integration client status`; add --overwrite-conflict to enable only if replacing it is intended.";
105
+ case "write_failed": return "The client config write failed. Run `ocx integration client status` before retrying.";
106
+ default: return undefined;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Integration writer detail is the only server prose this client appends beside the primary
112
+ * error. Scope it to the normalized request pathname (dot segments resolved, no query) and to
113
+ * an unredirected response so no other route's body can ride along.
114
+ */
115
+ function isIntegrationRoute(path: string, response: Response): boolean {
116
+ if (response.redirected) return false;
117
+ let pathname: string;
118
+ try { pathname = new URL(path.startsWith("/") ? path : "/" + path, "http://cli.invalid").pathname; }
119
+ catch { return false; }
120
+ return /^\/api\/client-integrations(?:\/|$)/.test(pathname);
121
+ }
122
+
88
123
  /**
89
124
  * Compose the operator-facing message from a management error body.
90
125
  *
@@ -102,7 +137,7 @@ function stringField(record: Record<string, unknown>, key: string): string | und
102
137
  * (#4662). Name the route instead, so any listener that does not serve a path stays legible
103
138
  * even if another one starts answering this way.
104
139
  */
105
- function responseMessage(body: unknown, status: number): string {
140
+ function responseMessage(body: unknown, status: number, integrationRoute = false): string {
106
141
  if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
107
142
  if (!body || typeof body !== "object") return `Management request failed (${status})`;
108
143
  const record = body as Record<string, unknown>;
@@ -119,6 +154,13 @@ function responseMessage(body: unknown, status: number): string {
119
154
  if (primary) break;
120
155
  }
121
156
  const parts = [primary ?? `Management request failed (${status})`];
157
+ // Integration mutation routes carry a writer explanation beside a generic error. Its prose is
158
+ // server text (paths, upstream errors), so render fixed guidance keyed on the closed reason
159
+ // code instead of echoing it; other management bodies keep their presentation contract.
160
+ if (integrationRoute && stringField(record, "message") && stringField(record, "message") !== primary) {
161
+ const fixed = integrationRecoveryMessage(stringField(record, "reason"));
162
+ if (fixed) parts.push(`Details: ${fixed}`);
163
+ }
122
164
  const reason = stringField(record, "reason");
123
165
  // A body of {ok:false, reason:"…"} with no `error` key used to degrade to the
124
166
  // generic line, discarding the only actionable field.
@@ -158,7 +200,9 @@ export async function runtimeRequest<T = unknown>(
158
200
  try { body = JSON.parse(text); }
159
201
  catch { body = text; }
160
202
  }
161
- if (!response.ok) throw new RuntimeApiError(responseMessage(body, response.status), response.status, body);
203
+ if (!response.ok) {
204
+ throw new RuntimeApiError(responseMessage(body, response.status, isIntegrationRoute(path, response)), response.status, body);
205
+ }
162
206
  return body as T;
163
207
  }
164
208
 
@@ -226,79 +270,6 @@ export function csv(value: string | undefined): string[] | undefined {
226
270
  return [...new Set(value.split(",").map(item => item.trim()).filter(Boolean))];
227
271
  }
228
272
 
229
- /**
230
- * Options whose VALUE is a credential (or can carry one), listed here so a parse
231
- * error never prints one. `--headers` belongs on the list defensively: custom
232
- * headers are documented as non-secret metadata and the validator rejects the
233
- * standard credential names, but it cannot recognize an arbitrary one such as
234
- * `X-My-Token`, so a parse error must not echo the value back either way.
235
- *
236
- * `takeOption` only understands `--flag value`. `--flag=value` therefore falls
237
- * through to `rejectArgs`, which reports the offending argument verbatim — for
238
- * `--code=https://…?code=SECRET` that writes the authorization code to stderr,
239
- * which is the exact exposure the stdin path exists to avoid.
240
- */
241
- const SECRET_OPTIONS = [
242
- "--code",
243
- "--headers",
244
- "--token",
245
- "--admin-token",
246
- "--pairing-code",
247
- "--credential-env",
248
- "--admin-token-env",
249
- "--pairing-code-env",
250
- ];
251
-
252
- /**
253
- * Replace credential values before they are reported back.
254
- *
255
- * Both spellings have to be covered, and the space-separated one spans two
256
- * tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
257
- * does not parse `--code` leaves the flag AND its value in the leftovers, and
258
- * reporting them verbatim writes the credential to stderr. Repeating the
259
- * option does the same with the second value, since the parser takes only the
260
- * first occurrence.
261
- *
262
- * The token after the option is redacted whatever it looks like. Skipping
263
- * `--`-prefixed tokens read as "that is a flag, not a value", but the shell
264
- * hands over whatever was typed: `--code --SUPERSECRET` and
265
- * `--code -- SUPERSECRET` both put the credential straight in the message. A
266
- * mistaken `--code --json` now reads `--code <redacted>`, which is worse
267
- * diagnostics for a case that already prints the usage text, and better than
268
- * printing a credential.
269
- *
270
- * `redactValues` extends that to bare leftovers, for commands whose positional
271
- * argument is itself a credential.
272
- */
273
- function redactSecretArgs(args: string[], redactValues = false): string[] {
274
- const out: string[] = [];
275
- for (let index = 0; index < args.length; index++) {
276
- const arg = args[index] as string;
277
- const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
278
- if (inline) {
279
- out.push(`${inline}=<redacted>`);
280
- continue;
281
- }
282
- if (SECRET_OPTIONS.includes(arg)) {
283
- out.push(arg);
284
- // Swallow the value that belongs to it. `--` is an end-of-options
285
- // separator, so the value is the token after it.
286
- let valueIndex = index + 1;
287
- if (args[valueIndex] === "--") {
288
- out.push("--");
289
- valueIndex++;
290
- }
291
- if (args[valueIndex] !== undefined) {
292
- out.push("<redacted>");
293
- index = valueIndex;
294
- }
295
- continue;
296
- }
297
- out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
298
- }
299
- return out;
300
- }
301
-
302
273
  export interface RejectArgsOptions {
303
274
  /**
304
275
  * Report bare leftovers as `<redacted>`. Set by commands where a stray
@@ -399,16 +370,19 @@ export async function readSecretBytes(
399
370
  deps: RuntimeApiDeps,
400
371
  label: string,
401
372
  maxBytes = MAX_LINK_CREDENTIAL_BYTES,
402
- ): Promise<Uint8Array> {
373
+ ): Promise<Uint8Array<ArrayBuffer>> {
403
374
  const input: CliStdin = deps.stdinImpl ?? process.stdin;
404
375
  const timeoutMs = deps.stdinTimeoutMs ?? 120_000;
376
+ const signal = deps.stdinSignal;
377
+ signal?.throwIfAborted();
405
378
  if (input.readableEnded === true) throw new CliUsageError(`${label} input was empty`);
406
- return await new Promise<Uint8Array>((resolve, reject) => {
379
+ return await new Promise<Uint8Array<ArrayBuffer>>((resolve, reject) => {
407
380
  const chunks: Uint8Array[] = [];
408
381
  let total = 0;
409
382
  let settled = false;
410
383
  const cleanup = () => {
411
384
  clearTimeout(timer);
385
+ signal?.removeEventListener("abort", onAbort);
412
386
  input.removeListener("data", onData);
413
387
  input.removeListener("end", onEnd);
414
388
  input.removeListener("error", onError);
@@ -459,6 +433,7 @@ export async function readSecretBytes(
459
433
  resolve(result);
460
434
  });
461
435
  const onError = (error: Error) => finish(() => reject(error));
436
+ const onAbort = () => finish(() => reject(signal?.reason));
462
437
  const timer = setTimeout(
463
438
  () => finish(() => reject(new CliUsageError(`timed out waiting for ${label} on stdin`))),
464
439
  timeoutMs,
@@ -466,6 +441,8 @@ export async function readSecretBytes(
466
441
  input.on("data", onData);
467
442
  input.on("end", onEnd);
468
443
  input.on("error", onError);
444
+ signal?.addEventListener("abort", onAbort, { once: true });
445
+ if (signal?.aborted) onAbort();
469
446
  });
470
447
  }
471
448
 
@@ -0,0 +1,149 @@
1
+ /** Credential-option redaction for CLI argument errors. Kept dependency-free so the CLI head can use it. */
2
+ /**
3
+ * Options whose VALUE is a credential (or can carry one), listed here so a parse
4
+ * error never prints one. `--headers` belongs on the list defensively: custom
5
+ * headers are documented as non-secret metadata and the validator rejects the
6
+ * standard credential names, but it cannot recognize an arbitrary one such as
7
+ * `X-My-Token`, so a parse error must not echo the value back either way.
8
+ *
9
+ * `takeOption` only understands `--flag value`. `--flag=value` therefore falls
10
+ * through to `rejectArgs`, which reports the offending argument verbatim — for
11
+ * `--code=https://…?code=SECRET` that writes the authorization code to stderr,
12
+ * which is the exact exposure the stdin path exists to avoid.
13
+ */
14
+ const SECRET_OPTIONS = [
15
+ "--code",
16
+ "--headers",
17
+ "--api-key",
18
+ "--key",
19
+ "--secret",
20
+ "--password",
21
+ "--token",
22
+ "--admin-token",
23
+ "--pairing-code",
24
+ "--credential-env",
25
+ "--admin-token-env",
26
+ "--pairing-code-env",
27
+ ];
28
+
29
+ function isSecretOptionToken(token: string): boolean {
30
+ return SECRET_OPTIONS.includes(token) || SECRET_OPTIONS.some(option => token.startsWith(`${option}=`));
31
+ }
32
+
33
+ let credentialArgvSeen = false;
34
+ let credentialValues: string[] = [];
35
+ let consoleScrubInstalled = false;
36
+
37
+ /** Operands of credential options in argv, longest first; very short values are not scrubbed. */
38
+ function credentialOperands(argv: readonly string[]): string[] {
39
+ const values = new Set<string>();
40
+ for (let index = 0; index < argv.length; index++) {
41
+ const token = argv[index] as string;
42
+ const inline = SECRET_OPTIONS.find(option => token.startsWith(`${option}=`));
43
+ let value: string | undefined;
44
+ if (inline) value = token.slice(inline.length + 1);
45
+ else if (SECRET_OPTIONS.includes(token)) {
46
+ value = argv[argv[index + 1] === "--" ? index + 2 : index + 1];
47
+ // A following bare credential option is not this option's operand; its own operand is
48
+ // collected on its turn. An inline value is always an operand, whatever it looks like.
49
+ if (value !== undefined && SECRET_OPTIONS.includes(value)) value = undefined;
50
+ }
51
+ if (value !== undefined && value.length >= 4) values.add(value);
52
+ }
53
+ return [...values].sort((a, b) => b.length - a.length);
54
+ }
55
+
56
+ /** Replace every argv credential operand in text printed by this process. */
57
+ export function scrubCredentialOperands(text: string): string {
58
+ let out = text;
59
+ // Case-insensitive: some parsers lowercase an action before echoing it.
60
+ for (const value of credentialValues) out = out.replace(new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "<redacted>");
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Last-line guard for console diagnostics (stderr): parsers that echo a selector, an unknown
66
+ * subcommand or a leftover before any redaction would otherwise print an operand typed after
67
+ * a credential option. Installed once; it reads the operands recorded for the current argv.
68
+ */
69
+ function installConsoleScrub(): void {
70
+ if (consoleScrubInstalled) return;
71
+ consoleScrubInstalled = true;
72
+ // Diagnostics only: stdout carries JSON documents whose primitives must not be rewritten.
73
+ for (const method of ["warn", "error"] as const) {
74
+ const original = console[method].bind(console);
75
+ console[method] = (...args: unknown[]) => {
76
+ if (credentialValues.length === 0) return original(...args);
77
+ return original(...args.map(arg => typeof arg === "string" ? scrubCredentialOperands(arg)
78
+ : arg instanceof Error ? scrubCredentialOperands(arg.stack ?? arg.message) : arg));
79
+ };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Record whether the process argv carried any credential option. Parsers that take
85
+ * positionals can consume the option token itself (`ocx account use openai --token X`
86
+ * makes `--token` the id), leaving only its operand as a "leftover"; once a credential
87
+ * option was present anywhere, every bare leftover is reported as `<redacted>`.
88
+ */
89
+ export function noteCredentialArgv(argv: readonly string[]): void {
90
+ credentialArgvSeen = argv.some(isSecretOptionToken);
91
+ credentialValues = credentialOperands(argv);
92
+ if (credentialValues.length > 0) installConsoleScrub();
93
+ }
94
+
95
+ /**
96
+ * Replace credential values before they are reported back.
97
+ *
98
+ * Both spellings have to be covered, and the space-separated one spans two
99
+ * tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
100
+ * does not parse `--code` leaves the flag AND its value in the leftovers, and
101
+ * reporting them verbatim writes the credential to stderr. Repeating the
102
+ * option does the same with the second value, since the parser takes only the
103
+ * first occurrence.
104
+ *
105
+ * The token after the option is redacted whatever it looks like. Skipping
106
+ * `--`-prefixed tokens read as "that is a flag, not a value", but the shell
107
+ * hands over whatever was typed: `--code --SUPERSECRET` and
108
+ * `--code -- SUPERSECRET` both put the credential straight in the message. A
109
+ * mistaken `--code --json` now reads `--code <redacted>`, which is worse
110
+ * diagnostics for a case that already prints the usage text, and better than
111
+ * printing a credential.
112
+ *
113
+ * `redactValues` extends that to bare leftovers, for commands whose positional
114
+ * argument is itself a credential.
115
+ */
116
+ export function redactSecretArgs(args: string[], redactValues = false): string[] {
117
+ redactValues ||= credentialArgvSeen;
118
+ const out: string[] = [];
119
+ for (let index = 0; index < args.length; index++) {
120
+ const arg = args[index] as string;
121
+ const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
122
+ if (inline) {
123
+ out.push(`${inline}=<redacted>`);
124
+ continue;
125
+ }
126
+ if (SECRET_OPTIONS.includes(arg)) {
127
+ out.push(arg);
128
+ // Swallow the value that belongs to it. `--` is an end-of-options
129
+ // separator, so the value is the token after it.
130
+ let valueIndex = index + 1;
131
+ if (args[valueIndex] === "--") {
132
+ out.push("--");
133
+ valueIndex++;
134
+ }
135
+ const next = args[valueIndex];
136
+ // A following credential option is not this option's value: leave it for the
137
+ // next iteration so its own operand is redacted too (`--code --token SECRET`).
138
+ if (next !== undefined && !isSecretOptionToken(next)) {
139
+ out.push("<redacted>");
140
+ index = valueIndex;
141
+ } else if (next !== undefined) {
142
+ index = valueIndex - 1;
143
+ }
144
+ continue;
145
+ }
146
+ out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
147
+ }
148
+ return out;
149
+ }
@@ -0,0 +1,30 @@
1
+ import { printData } from "./runtime-api";
2
+ import type { CodexDesktopSwitchApply, CodexDesktopSwitchApplyReason } from "../codex/desktop-switches";
3
+
4
+ const APPLY_REASONS = {
5
+ not_requested: true, proxy_not_running: true, integration_disabled: true,
6
+ external_provider: true, ownership_undetermined: true, write_lock_busy: true, injection_refused: true,
7
+ } satisfies Record<CodexDesktopSwitchApplyReason, true>;
8
+
9
+ /** The shared native apply DTO, excluding exception/path detail. */
10
+ export function projectSettingsApply(value: unknown): CodexDesktopSwitchApply | null {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12
+ const row = value as Record<string, unknown>;
13
+ if (row.applied === true) return { applied: true };
14
+ if (row.applied !== false || typeof row.reason !== "string" || !Object.hasOwn(APPLY_REASONS, row.reason)
15
+ || typeof row.retryable !== "boolean") return null;
16
+ return { applied: false, reason: row.reason as CodexDesktopSwitchApplyReason, retryable: row.retryable };
17
+ }
18
+
19
+ /** Caller rebuilds the relevant task DTO; settings pending is not a CatalogDisposition. */
20
+ export function printSettingsResult(
21
+ data: Record<string, unknown>, pending: unknown, wantsJson: boolean, lines: readonly string[],
22
+ ): number {
23
+ const verified = typeof pending === "boolean";
24
+ const note = !verified ? "Settings were accepted; catalog status is unverified. Read back before retrying."
25
+ : pending ? "Settings saved; catalog refresh is pending."
26
+ : "Settings saved; this response reports no pending catalog refresh.";
27
+ printData({ ...data, catalogRefreshPending: verified ? pending : null,
28
+ ...(!verified ? { verification: "unverified" } : {}) }, wantsJson, [...lines, note]);
29
+ return verified && pending === false ? 0 : 1;
30
+ }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import {
19
19
  CliUsageError,
20
+ RuntimeApiError,
20
21
  printData,
21
22
  rejectArgs,
22
23
  runCliAction,
@@ -27,6 +28,7 @@ import {
27
28
  takeOption,
28
29
  type RuntimeApiDeps,
29
30
  } from "./runtime-api";
31
+ import type { StorageCleanupPolicy } from "../types/config";
30
32
 
31
33
  const USAGE = `Usage:
32
34
  ocx storage report [--json]
@@ -34,7 +36,8 @@ const USAGE = `Usage:
34
36
  ocx storage trash [list] [--json]
35
37
  ocx storage trash restore <entry-id> [--yes] [--json]
36
38
  ocx storage policy [show] [--json]
37
- ocx storage policy set [--enabled <true|false>] [--percent <0-100>]
39
+ ocx storage policy set [--enabled <true|false>] [--archived-bytes-over <bytes>]
40
+ [--reduce-to-bytes <bytes> | --remove-oldest-percent <1-100> | --percent <1-100>]
38
41
  [--mode <quarantine|permanent>] [--schedule <startup|daily|weekly|manual>] [--json]
39
42
  ocx storage policy run [--yes] [--json]
40
43
 
@@ -146,6 +149,41 @@ async function trash(argv: string[], deps: RuntimeApiDeps): Promise<void> {
146
149
  printData(result, wantsJson, summaryLines(result));
147
150
  }
148
151
 
152
+ /** New policy controls expose only observed settings and job status, never arbitrary API fields. */
153
+ type ObservedPolicy = Omit<StorageCleanupPolicy, "target"> & { target: { reduceToBytes: number } | { removeOldestPercent: number } };
154
+ function policyWriteReceipt(value: unknown): { ok: true; policy: ObservedPolicy; job: { status: "idle" | "running" } } {
155
+ const record = (item: unknown): item is Record<string, unknown> => item !== null && typeof item === "object" && !Array.isArray(item);
156
+ const count = (item: unknown): item is number => typeof item === "number" && Number.isInteger(item) && item >= 0;
157
+ const invalid = (): never => { throw new Error("Invalid storage policy response; inspect the policy before retrying."); };
158
+ if (!record(value) || value.ok !== true || !record(value.policy) || !record(value.job)) return invalid();
159
+ const source = value.policy;
160
+ if (typeof source.enabled !== "boolean" || !record(source.trigger) || !count(source.trigger.archivedBytesOver)
161
+ || !record(source.target) || (source.mode !== "quarantine" && source.mode !== "permanent")
162
+ || (source.schedule !== "startup" && source.schedule !== "daily" && source.schedule !== "weekly" && source.schedule !== "manual")
163
+ || (value.job.status !== "idle" && value.job.status !== "running")) return invalid();
164
+ const { reduceToBytes, removeOldestPercent } = source.target;
165
+ let target: ObservedPolicy["target"];
166
+ if (count(reduceToBytes) && removeOldestPercent === undefined) target = { reduceToBytes };
167
+ else if (count(removeOldestPercent) && removeOldestPercent >= 1 && removeOldestPercent <= 100 && reduceToBytes === undefined) target = { removeOldestPercent };
168
+ else return invalid();
169
+ const policy: ObservedPolicy = {
170
+ enabled: source.enabled,
171
+ trigger: { archivedBytesOver: source.trigger.archivedBytesOver },
172
+ target,
173
+ mode: source.mode,
174
+ schedule: source.schedule,
175
+ };
176
+ if (source.lastRun !== undefined) {
177
+ if (!record(source.lastRun) || !count(source.lastRun.at) || !count(source.lastRun.freedBytes) || !count(source.lastRun.removed)) return invalid();
178
+ policy.lastRun = { at: source.lastRun.at, freedBytes: source.lastRun.freedBytes, removed: source.lastRun.removed };
179
+ }
180
+ if (source.nextRun !== undefined) {
181
+ if (!count(source.nextRun)) return invalid();
182
+ policy.nextRun = source.nextRun;
183
+ }
184
+ return { ok: true, policy, job: { status: value.job.status } };
185
+ }
186
+
149
187
  async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
188
  const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "show";
151
189
  const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
@@ -164,6 +202,9 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
164
202
  const wantsJson = takeFlag(args, "--json");
165
203
  const enabled = takeOption(args, "--enabled");
166
204
  const percent = takeIntegerOption(args, "--percent", { min: 0 });
205
+ const removeOldestPercent = takeIntegerOption(args, "--remove-oldest-percent", { min: 0 });
206
+ const reduceToBytes = takeIntegerOption(args, "--reduce-to-bytes", { min: 0 });
207
+ const archivedBytesOver = takeIntegerOption(args, "--archived-bytes-over", { min: 0 });
167
208
  const mode = takeOption(args, "--mode");
168
209
  const schedule = takeOption(args, "--schedule");
169
210
  rejectArgs(args, USAGE);
@@ -171,8 +212,13 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
171
212
  if (enabled !== undefined && enabled !== "true" && enabled !== "false") {
172
213
  throw new CliUsageError("--enabled must be true or false", USAGE);
173
214
  }
215
+ if ([percent, removeOldestPercent, reduceToBytes].filter(value => value !== undefined).length > 1) {
216
+ throw new CliUsageError("choose one target: --percent, --remove-oldest-percent or --reduce-to-bytes", USAGE);
217
+ }
218
+ const extended = archivedBytesOver !== undefined || reduceToBytes !== undefined || removeOldestPercent !== undefined;
174
219
  const body: Record<string, unknown> = {};
175
220
  if (enabled !== undefined) body.enabled = enabled === "true";
221
+ if (archivedBytesOver !== undefined) body.trigger = { archivedBytesOver };
176
222
  // The policy target is nested. A top-level `percent` is not part of the PUT contract:
177
223
  // `normalizeStorageCleanupPolicy` reads only `target`, so the field was dropped and the
178
224
  // previously stored target survived. `--percent 10` on a policy still holding the
@@ -183,20 +229,44 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
183
229
  // vocabulary and answers with a named 400, which is a rejected write rather than the
184
230
  // silent wrong write this replaces.
185
231
  if (percent !== undefined) body.target = { removeOldestPercent: percent };
232
+ if (removeOldestPercent !== undefined) body.target = { removeOldestPercent };
233
+ if (reduceToBytes !== undefined) body.target = { reduceToBytes };
186
234
  if (mode !== undefined) body.mode = mode;
187
235
  if (schedule !== undefined) body.schedule = schedule;
188
236
  if (Object.keys(body).length === 0) {
189
- throw new CliUsageError("policy set needs at least one of --enabled, --percent, --mode, --schedule", USAGE);
237
+ throw new CliUsageError("policy set needs at least one policy field", USAGE);
190
238
  }
191
239
  // Values are NOT re-validated here beyond --enabled's shape. The server owns the mode and
192
240
  // schedule vocabularies and returns a named 400; duplicating them is a second thing to
193
241
  // keep in sync. `enabled` is checked because "--enabled maybe" would otherwise be sent as
194
242
  // `false`, which is a wrong write rather than a rejected one.
195
- const result = await runtimeRequest("/api/storage/cleanup-policy", {
196
- method: "PUT",
197
- headers: { "content-type": "application/json" },
198
- body: JSON.stringify(body),
199
- }, deps);
243
+ let result: unknown;
244
+ try {
245
+ result = await runtimeRequest("/api/storage/cleanup-policy", {
246
+ method: "PUT",
247
+ ...(extended ? { redirect: "error" as const } : {}),
248
+ headers: { "content-type": "application/json" },
249
+ body: JSON.stringify(body),
250
+ }, deps);
251
+ } catch (error) {
252
+ if (!extended) throw error;
253
+ if (error instanceof RuntimeApiError) {
254
+ throw new RuntimeApiError(`Storage policy request failed (${error.status}); inspect the policy before retrying.`, error.status, null);
255
+ }
256
+ throw new Error("Storage policy request failed; inspect the policy before retrying.");
257
+ }
258
+ if (extended) {
259
+ const receipt = policyWriteReceipt(result);
260
+ const saved = receipt.policy;
261
+ printData(receipt, wantsJson, [
262
+ "Policy saved. This command did not start cleanup.",
263
+ `Enabled: ${saved.enabled}`,
264
+ `Trigger: archived bytes over ${saved.trigger.archivedBytesOver}`,
265
+ "reduceToBytes" in saved.target ? `Target: reduce archives to ${saved.target.reduceToBytes} bytes` : `Target: remove oldest ${saved.target.removeOldestPercent}%`,
266
+ `Mode: ${saved.mode}; schedule: ${saved.schedule}; job: ${receipt.job.status}`,
267
+ ]);
268
+ return;
269
+ }
200
270
  printData(result, wantsJson, summaryLines(result));
201
271
  return;
202
272
  }
@@ -1,3 +1,4 @@
1
+ import { runCatalogAction } from "./catalog-command-result";
1
2
  import {
2
3
  CliUsageError,
3
4
  desktopSwitchApplyReason,
@@ -14,8 +15,11 @@ import {
14
15
 
15
16
  const USAGE = `Usage:
16
17
  ocx system [status] [--json]
18
+ ocx system health [--json]
17
19
  ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
18
- [--desktop-authless <on|off>] [--client-compaction <on|off>] [--json]
20
+ [--desktop-authless <on|off>] [--client-compaction <on|off>]
21
+ [--show-codex-credits <on|off>] [--account-picker <on|off>] [--main-account-hard-lock <on|off>]
22
+ [--ultra-fast-tier <on|off>] [--fast-rows <on|off>] [--json]
19
23
  ocx system startup <health|install-service|install-shim> [--json]
20
24
  ocx system diagnostics [--json]
21
25
  ocx system sync [--json]
@@ -45,6 +49,46 @@ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
45
49
  printData(result, wantsJson, summaryLines(result));
46
50
  }
47
51
 
52
+ /** The management health observation is distinct from root liveness and aggregate status. */
53
+ async function health(argv: string[], deps: RuntimeApiDeps): Promise<number> {
54
+ return runCatalogAction(async () => {
55
+ const args = [...argv], wantsJson = takeFlag(args, "--json");
56
+ if (args.length) throw new CliUsageError("system health accepts only --json", USAGE);
57
+ const raw = await runtimeRequest("/api/system/health", { method: "GET", redirect: "error" }, deps);
58
+ const value = recordValue(raw), ledger = recordValue(value?.spendLedger);
59
+ const counter = (number: unknown): number => {
60
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0 || number > 1_000_000) {
61
+ throw new Error("Invalid health counter");
62
+ }
63
+ return number;
64
+ };
65
+ if (!value || Array.isArray(raw) || !ledger || Array.isArray(value.spendLedger)
66
+ || value.status !== "ok" || value.service !== "opencodex"
67
+ || typeof value.version !== "string" || !value.version.trim()
68
+ || typeof value.uptime !== "number" || !Number.isFinite(value.uptime) || value.uptime < 0
69
+ || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
70
+ || (ledger.ownership !== "held" && ledger.ownership !== "unheld")
71
+ || typeof ledger.initialized !== "boolean" || typeof ledger.configured !== "boolean"
72
+ || typeof ledger.degraded !== "boolean") throw new Error("Invalid system health response");
73
+ const result = {
74
+ status: "ok", service: "opencodex", version: value.version, uptime: value.uptime, pid: value.pid,
75
+ spendLedger: {
76
+ ownership: ledger.ownership, initialized: ledger.initialized, configured: ledger.configured,
77
+ degraded: ledger.degraded, persistFailures: counter(ledger.persistFailures), corruptRecords: counter(ledger.corruptRecords),
78
+ },
79
+ };
80
+ printData(result, wantsJson, [
81
+ `Management endpoint: ${result.status} (${result.service} ${result.version}, PID ${result.pid})`,
82
+ `Uptime: ${result.uptime} seconds`,
83
+ `Spend ledger: ${ledger.degraded ? "degraded" : "no degradation reported"}; ownership ${ledger.ownership}`,
84
+ `Ledger initialized: ${ledger.initialized}; configured: ${ledger.configured}`,
85
+ `Ledger persistence failures: ${result.spendLedger.persistFailures}; corrupt records: ${result.spendLedger.corruptRecords}`,
86
+ "This observation does not certify every subsystem healthy.",
87
+ ...(ledger.degraded ? ["Next: inspect the proxy diagnostics and spend-ledger configuration before relying on spend limits."] : []),
88
+ ]);
89
+ });
90
+ }
91
+
48
92
  function recordValue(value: unknown): Record<string, unknown> | undefined {
49
93
  return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
50
94
  }
@@ -187,11 +231,25 @@ async function update(argv: string[], deps: RuntimeApiDeps): Promise<void> {
187
231
  if (!yes) throw new CliUsageError("update run requires --yes", USAGE);
188
232
  rejectArgs(args, USAGE);
189
233
  const result = await runtimeRequest("/api/update/run", { method: "POST", body: JSON.stringify({ tag: channel, restart }) }, deps);
190
- printData(result, wantsJson, [`Update started (${channel}).`]);
234
+ const job = recordValue(recordValue(result)?.job);
235
+ const jobId = typeof job?.id === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(job.id) ? job.id : undefined;
236
+ const jobStatus = ["running", "restarting", "succeeded", "failed"].find(state => state === job?.status);
237
+ const lines = jobId ? [
238
+ `Update job ${jobId}: ${jobStatus ?? "status unconfirmed"} (${channel}).`,
239
+ `Check progress: ocx system update status ${jobId}`,
240
+ ] : ["Update request returned no usable job ID. Check: ocx system update check."];
241
+ printData(result, wantsJson, lines);
191
242
  }
192
243
 
193
244
  export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
194
245
  const [sub = "status", ...rest] = argv;
246
+ if (sub === "health") return health(rest, deps);
247
+ if (sub === "settings") {
248
+ const { SYSTEM_PARITY_OPTIONS, handleSystemSettingsParity } = await import("./system-settings-parity");
249
+ if (rest.some(arg => SYSTEM_PARITY_OPTIONS.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
250
+ return handleSystemSettingsParity(rest, deps);
251
+ }
252
+ }
195
253
  if (sub === "codex-cli-update") {
196
254
  const { handleCodexCliUpdateCommand } = await import("./codex-cli-update");
197
255
  return await handleCodexCliUpdateCommand(rest);