@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
@@ -14,7 +14,7 @@ const USAGE = `Usage:
14
14
  ocx config set <dot.path> <json-or-string> [--json]
15
15
  ocx config unset <dot.path> [--json]
16
16
  ocx config validate [path|-] [--json]
17
- ocx config export <path|->
17
+ ocx config export <path|-> [--json]
18
18
  ocx config import <path|-> --yes [--json]`;
19
19
 
20
20
  /**
@@ -205,14 +205,21 @@ function validate(value: unknown): OcxConfig {
205
205
  }
206
206
 
207
207
  export async function handleConfigCommand(argv: string[]): Promise<number> {
208
- return runCliAction(async () => {
208
+ let outcome = 0;
209
+ const status = await runCliAction(async () => {
209
210
  const args = [...argv];
210
- const action = (args.shift() ?? "show").toLowerCase();
211
211
  const wantsJson = takeFlag(args, "--json");
212
+ const source = takeFlag(args, "--source");
213
+ if (args.includes("--json") || args.includes("--source")) throw new CliUsageError("config flags may only be specified once", USAGE);
214
+ const action = (args.shift() ?? "show").toLowerCase();
215
+ if (source && action !== "show") throw new CliUsageError("--source is only supported for config show", USAGE);
212
216
  if (action === "show") {
213
- const source = takeFlag(args, "--source");
214
217
  rejectArgs(args, USAGE);
215
218
  const diagnostics = readConfigDiagnostics();
219
+ if (diagnostics.source === "fallback" || diagnostics.error) {
220
+ console.error(`Warning: Config at ${getConfigPath()} is invalid or unreadable; defaults are being shown for invalid settings. Run: ocx config validate. Inspect: ocx config show --source.`);
221
+ if (!source) outcome = 1;
222
+ }
216
223
  const redacted = redact(diagnostics.config);
217
224
  const note = await readRemoteHubConfigNote(diagnostics.config);
218
225
  // First key, not last: it has to be read before the empty `providers` map that misled a
@@ -228,7 +235,12 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
228
235
  const path = args.shift();
229
236
  if (!path) throw new CliUsageError("config path is required", USAGE);
230
237
  rejectArgs(args, USAGE);
231
- const value = redact(getPath(readConfigDiagnostics().config, path), pathSegments(path).at(-1));
238
+ const diagnostics = readConfigDiagnostics();
239
+ if (diagnostics.source === "fallback" || diagnostics.error) {
240
+ console.error(`Warning: Config at ${getConfigPath()} is invalid or unreadable; defaults are being shown for invalid settings. Run: ocx config validate. Inspect: ocx config show --source.`);
241
+ outcome = 1;
242
+ }
243
+ const value = redact(getPath(diagnostics.config, path), pathSegments(path).at(-1));
232
244
  if (wantsJson || typeof value === "object") console.log(JSON.stringify(value, null, 2));
233
245
  else console.log(String(value));
234
246
  return;
@@ -236,7 +248,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
236
248
  if (action === "set" || action === "unset") {
237
249
  const path = args.shift();
238
250
  const raw = action === "set" ? args.shift() : undefined;
239
- if (!path || (action === "set" && raw === undefined)) throw new CliUsageError("config path and value are required", USAGE);
251
+ if (!path || (action === "set" && raw === undefined)) throw new CliUsageError(action === "unset" ? "config path is required" : "config path and value are required", USAGE);
240
252
  rejectArgs(args, USAGE);
241
253
  // #1835/#1838: the read used to happen OUTSIDE the mutation lock, so a concurrent
242
254
  // edit landing between it and the save was reverted by this whole-snapshot write.
@@ -288,7 +300,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
288
300
  })();
289
301
  printData(result.ok ? { ok: true, source: path ?? getConfigPath() } : result, wantsJson,
290
302
  [result.ok ? "Config is valid." : `Config is invalid: ${result.error}`]);
291
- if (!result.ok) process.exitCode = 1;
303
+ if (!result.ok) outcome = 1;
292
304
  return;
293
305
  }
294
306
  if (action === "export") {
@@ -297,7 +309,10 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
297
309
  rejectArgs(args, USAGE);
298
310
  const content = `${JSON.stringify(readConfigDiagnostics().config, null, 2)}\n`;
299
311
  if (path === "-") process.stdout.write(content);
300
- else { writeFileSync(path, content, { encoding: "utf8", mode: 0o600 }); console.log(`Exported config to ${path}.`); }
312
+ else {
313
+ writeFileSync(path, content, { encoding: "utf8", mode: 0o600 });
314
+ printData({ ok: true, path }, wantsJson, [`Exported config to ${path}.`]);
315
+ }
301
316
  return;
302
317
  }
303
318
  if (action === "import") {
@@ -312,6 +327,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
312
327
  }
313
328
  throw new CliUsageError(`unknown config command ${action}`, USAGE);
314
329
  });
330
+ return status || outcome;
315
331
  }
316
332
 
317
333
  export const CONFIG_USAGE = USAGE;
@@ -10,8 +10,7 @@ import type { ProxyRestartStartOutcome } from "./tray-proxy";
10
10
  * never needs to import the entry module back (no cycle).
11
11
  */
12
12
  import { CLI_COMMANDS } from "./registry";
13
- import { isValidProviderName } from "../config/provider-name";
14
- import type { CliHead } from "./root";
13
+ import { uninstallArgsError, type CliHead } from "./root";
15
14
  import type { ReadyArgs } from "./ready";
16
15
  import type { LivenessIo, LiveProxy } from "../server/proxy-liveness";
17
16
  import type { OcxConfig } from "../types";
@@ -300,6 +299,11 @@ const commandRunners: Record<string, CommandRunner> = {
300
299
  return Number(process.exitCode ?? 0);
301
300
  },
302
301
  uninstall: async deps => {
302
+ const error = uninstallArgsError("uninstall", deps.args);
303
+ if (error) {
304
+ console.error(error);
305
+ return 2;
306
+ }
303
307
  await deps.handleUninstall();
304
308
  return Number(process.exitCode ?? 0);
305
309
  },
@@ -376,62 +380,8 @@ const commandRunners: Record<string, CommandRunner> = {
376
380
  return 0;
377
381
  },
378
382
  logout: async deps => {
379
- // Argv is parsed BEFORE any store access, which is the whole point of this shape.
380
- // Previously `args[1]` was taken as the provider name with no parsing, so
381
- // `ocx logout --json` called removeCredential("--json"), printed "Logged out of
382
- // --json." and exited 0 -- a silent false success, the worst outcome for a caller
383
- // that can only see the exit code.
384
- //
385
- // That is not merely a wasted call. `normalizeAuthStore` copies every top-level key
386
- // it finds, so a hand-edited, legacy, or corrupted auth.json containing a `--json`
387
- // key would have its active account deleted -- and the key dropped entirely if that
388
- // was its last account. A flag must never reach the store as a provider name.
389
- const logoutArgs = deps.args.slice(1);
390
- const wantsJson = logoutArgs.includes("--json");
391
- // Any leading dash is an option, not a provider. Matching only `--` left the same defect
392
- // one dash shorter: `ocx logout -j` treated `-j` as the provider name and, with a `-j` key
393
- // present in the store, deleted it and exited 0.
394
- const isOption = (arg: string): boolean => arg.startsWith("-");
395
- const positionals = logoutArgs.filter(arg => !isOption(arg));
396
- const unknownFlags = logoutArgs.filter(arg => isOption(arg) && arg !== "--json");
397
- const name = (positionals[0] ?? "").trim().toLowerCase();
398
-
399
- // Usage failures exit 2 and touch nothing. A missing provider is a usage error; a
400
- // provider that simply has no credential is a not-found (4) further down, because the
401
- // vocabulary distinguishes "you called this wrong" from "the thing is not there".
402
- //
403
- // The shape check is `isValidProviderName`, not another dash test. Rejecting a leading
404
- // ASCII `-` fixed `-j` and still let `logout —json` through with a Unicode dash, which is
405
- // the same defect a third time: each patch named one spelling instead of the class. The
406
- // canonical validator states the rule positively -- start and end alphanumeric, internal
407
- // `._-` allowed -- so `github-copilot` and `google-antigravity` pass while every dash
408
- // variant, empty string, and reserved name fails. Anything that is not a possible
409
- // provider id cannot reach the store at all.
410
- const malformedName = Boolean(name) && !isValidProviderName(name);
411
- if (unknownFlags.length > 0 || positionals.length > 1 || !name || malformedName) {
412
- const problem = unknownFlags.length > 0
413
- ? `unknown option ${unknownFlags[0]}`
414
- : positionals.length > 1 ? "too many arguments"
415
- : malformedName ? `not a valid provider name: ${name}`
416
- : "missing provider";
417
- console.error(`Usage: ocx logout <provider> [--json] (${problem})`);
418
- return 2;
419
- }
420
-
421
- // The disposition comes from inside the store mutation, not from a read-then-remove
422
- // preflight. `mutateStore` serializes writes, so a preflight leaves a window where a
423
- // concurrent logout removes the same account and BOTH callers exit 0 claiming a removal --
424
- // a false success again, just a narrower one than the flag bug above.
425
- const { removeCredential } = await import("../oauth/store");
426
- const outcome = await removeCredential(name);
427
- if (outcome === "not-found") {
428
- if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: false, provider: name, removed: false, reason: "not_found" }, null, 2));
429
- else console.error(`No stored credential for '${name}'.`);
430
- return 4;
431
- }
432
- if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: true, provider: name, removed: true }, null, 2));
433
- else console.log(`Logged out of ${name}.`);
434
- return 0;
383
+ const { handleLogoutCommand } = await import("./logout-command");
384
+ return handleLogoutCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
435
385
  },
436
386
  sync: async deps => {
437
387
  const syncArgs = deps.args.slice(1);
@@ -538,7 +488,7 @@ const commandRunners: Record<string, CommandRunner> = {
538
488
  },
539
489
  v2: async deps => {
540
490
  const { cmdV2 } = await import("./v2");
541
- return await cmdV2(deps.args.slice(1), {}, async () => (await deps.findLiveProxy())?.port);
491
+ return await cmdV2(deps.args.slice(1), { runtimeApi: { findLiveProxy: deps.findLiveProxy } }, async () => (await deps.findLiveProxy())?.port);
542
492
  },
543
493
  connect: async deps => {
544
494
  const { handleConnectCommand } = await import("./connect");
@@ -550,7 +500,7 @@ const commandRunners: Record<string, CommandRunner> = {
550
500
  },
551
501
  "remote-workspace": async deps => {
552
502
  const { runRemoteWorkspaceCommand } = await import("./remote-workspace");
553
- return await runRemoteWorkspaceCommand(deps.args.slice(1));
503
+ return await runRemoteWorkspaceCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
554
504
  },
555
505
  disconnect: async deps => {
556
506
  const { handleDisconnectCommand } = await import("./connect");
@@ -570,7 +520,8 @@ const commandRunners: Record<string, CommandRunner> = {
570
520
  const cacheGateSnapshot = deps.loadConfig();
571
521
  const desiredDisabled = !shouldSyncCodexOnStart(cacheGateSnapshot);
572
522
  const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
573
- invalidateCodexModelsCacheWithPermitOutcome(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
523
+ invalidateCodexModelsCacheWithPermitOutcome(permit, owningCodexHome, { allowWhenDesiredDisabled: true }),
524
+ { intent: "cache", writer: "sync-cache" });
574
525
  const cacheJson = cacheArgs.includes("--json");
575
526
  const jsonSafeLog = cacheJson
576
527
  ? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
@@ -633,6 +584,10 @@ const commandRunners: Record<string, CommandRunner> = {
633
584
  console.log("No Codex catalog to derive a cache from; nothing to sync.");
634
585
  } else if (unchanged) {
635
586
  console.log("Codex model cache is already current; nothing to sync.");
587
+ } else if (invalidated.kind === "unavailable"
588
+ && (invalidated.reason === "foreign-owner" || invalidated.reason === "owner-unknown")) {
589
+ const { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE } = await import("../codex/catalog/routed-removal");
590
+ console.error(invalidated.reason === "foreign-owner" ? FOREIGN_CODEX_HOME_OWNER_MESSAGE : UNKNOWN_CODEX_HOME_OWNER_MESSAGE);
636
591
  } else if (!ok) {
637
592
  console.error(`Cache refresh did not complete (${invalidated.kind}). The Codex model cache was not rewritten.`);
638
593
  }
@@ -697,19 +652,33 @@ const commandRunners: Record<string, CommandRunner> = {
697
652
  switch (deps.args[1]) {
698
653
  case "install": {
699
654
  const r = installCodexShim();
700
- const { healthy, summary } = diagnoseCodexShim();
655
+ const { healthy, runnable, active, summary } = diagnoseCodexShim();
656
+ const success = !r.refused && (runnable ?? healthy);
701
657
  const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness");
702
- const warnings = healthy
658
+ const warnings = success
703
659
  ? collectCodexShimReadinessWarnings()
704
660
  : [];
705
- console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
661
+ console.log(`${success && healthy && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
706
662
  for (const warning of warnings) console.warn(` ${warning}`);
707
- if (!healthy) console.error(`Codex shim installation is unhealthy: ${summary}`);
708
- return healthy ? 0 : 1;
663
+ if (success && active !== undefined && active !== true) {
664
+ const { overlayActivationHint } = await import("../codex/shim-overlay");
665
+ console.warn(` ${overlayActivationHint()}`);
666
+ }
667
+ if (!success) console.error(`${r.refused ? "Codex shim installation was refused" : "Codex shim installation is unhealthy"}: ${summary}`);
668
+ return success ? 0 : 1;
709
669
  }
710
- case "status":
670
+ case "status": {
671
+ const extra = deps.args.slice(2);
672
+ if (extra.length > 0) {
673
+ console.error(extra.some(isJsonOption)
674
+ ? "ocx codex-shim status does not support --json; use ocx status --json (codexShim)."
675
+ : "ocx codex-shim status does not accept arguments or options.");
676
+ console.error("Usage: ocx codex-shim status");
677
+ return 2;
678
+ }
711
679
  console.log(codexShimStatus());
712
680
  break;
681
+ }
713
682
  case "uninstall":
714
683
  case "remove": {
715
684
  const r = uninstallCodexShim();
@@ -731,7 +700,7 @@ const commandRunners: Record<string, CommandRunner> = {
731
700
  }
732
701
  const { runUpdate } = await import("../update");
733
702
  await runUpdate();
734
- return 0;
703
+ return Number(process.exitCode ?? 0);
735
704
  },
736
705
  "__refresh-version": async deps => {
737
706
  // Hidden, detached helper spawned by the update prompt to refresh the
@@ -786,6 +755,10 @@ const commandRunners: Record<string, CommandRunner> = {
786
755
  },
787
756
  health: async deps => {
788
757
  const healthArgs = deps.args.slice(1);
758
+ if (healthArgs.length > 1 || (healthArgs.length === 1 && healthArgs[0] !== "--json")) {
759
+ console.error("Usage: ocx health [--json]\nSee: ocx help health");
760
+ return 2;
761
+ }
789
762
  const wantsHealthJson = healthArgs.includes("--json");
790
763
  // A proxy that has only just bound can miss a single probe while its event loop
791
764
  // is still settling startup work — the same just-started race the stop paths
@@ -844,7 +817,7 @@ const commandRunners: Record<string, CommandRunner> = {
844
817
  },
845
818
  companion: async deps => {
846
819
  const { handleCompanionCommand } = await import("./companion");
847
- return await handleCompanionCommand(deps.args.slice(1));
820
+ return await handleCompanionCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
848
821
  },
849
822
  route: async deps => {
850
823
  if (deps.args[1] !== "combo" && deps.args[1] !== "policy") {
@@ -869,7 +842,7 @@ const commandRunners: Record<string, CommandRunner> = {
869
842
  },
870
843
  observe: async deps => {
871
844
  const { handleObserveCommand } = await import("./observe");
872
- return await handleObserveCommand(deps.args.slice(1));
845
+ return await handleObserveCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
873
846
  },
874
847
  inspect: async deps => {
875
848
  const { handleInspectCommand } = await import("./inspect");
@@ -877,11 +850,11 @@ const commandRunners: Record<string, CommandRunner> = {
877
850
  },
878
851
  logs: async deps => {
879
852
  const { handleObserveCommand } = await import("./observe");
880
- return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
853
+ return await handleObserveCommand([deps.command!, ...deps.args.slice(1)], { findLiveProxy: deps.findLiveProxy });
881
854
  },
882
855
  usage: async deps => {
883
856
  const { handleObserveCommand } = await import("./observe");
884
- return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
857
+ return await handleObserveCommand([deps.command!, ...deps.args.slice(1)], { findLiveProxy: deps.findLiveProxy });
885
858
  },
886
859
  storage: async deps => {
887
860
  // `ocx storage` used to be a pure alias of `observe storage`, which reached only the report
@@ -897,11 +870,11 @@ const commandRunners: Record<string, CommandRunner> = {
897
870
  },
898
871
  access: async deps => {
899
872
  const { handleAccessCommand } = await import("./access");
900
- return await handleAccessCommand(deps.args.slice(1));
873
+ return await handleAccessCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
901
874
  },
902
875
  "api-key": async deps => {
903
876
  const { handleAccessCommand } = await import("./access");
904
- return await handleAccessCommand(["key", ...deps.args.slice(1)]);
877
+ return await handleAccessCommand(["key", ...deps.args.slice(1)], { findLiveProxy: deps.findLiveProxy });
905
878
  },
906
879
  api: async deps => {
907
880
  const { handleApiCommand } = await import("./api-protocols");
@@ -925,21 +898,21 @@ const commandRunners: Record<string, CommandRunner> = {
925
898
  // integrations `client` manages, so they get their own subcommand rather than being
926
899
  // folded into one that means something else.
927
900
  const { handleIntegrationCommand } = await import("./inspect");
928
- return await handleIntegrationCommand(deps.args.slice(1));
901
+ return await handleIntegrationCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
929
902
  } else if (integration === "claude") {
930
903
  const { handleClaudeConfigCommand } = await import("./integrations");
931
904
  return await handleClaudeConfigCommand(deps.args.slice(2));
932
905
  } else if (integration === "client") {
933
906
  const { handleClientIntegrationCommand } = await import("./integrations");
934
- return await handleClientIntegrationCommand(deps.args.slice(2));
907
+ return await handleClientIntegrationCommand(deps.args.slice(2), { findLiveProxy: deps.findLiveProxy });
935
908
  } else {
936
- console.error("Usage: ocx integration <claude|grok|client> <subcommand>");
909
+ console.error("Usage: ocx integration <claude|grok|client|native> <subcommand>\nSee: ocx help integration");
937
910
  return 2;
938
911
  }
939
912
  },
940
913
  system: async deps => {
941
914
  const { handleSystemCommand } = await import("./system-command");
942
- return await handleSystemCommand(deps.args.slice(1));
915
+ return await handleSystemCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
943
916
  },
944
917
  config: async deps => {
945
918
  const { handleConfigCommand } = await import("./config-command");
@@ -958,7 +931,7 @@ const commandRunners: Record<string, CommandRunner> = {
958
931
  // "ocx claude desktop" → write Desktop 3P config
959
932
  if (deps.args[1] === "desktop") {
960
933
  const { handleClaudeDesktopCommand } = await import("./claude-desktop");
961
- const exitCode = await handleClaudeDesktopCommand(deps.args.slice(2));
934
+ const exitCode = await handleClaudeDesktopCommand(deps.args.slice(2), { findLiveProxyImpl: deps.findLiveProxy });
962
935
  if (exitCode !== 0) return exitCode;
963
936
  return 0;
964
937
  }
package/src/cli/doctor.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  } from "../codex/subagent-model-fallback";
34
34
  import { readCatalog, readCodexCatalogPath, readConfiguredDefaultModel } from "../codex/catalog/parsing";
35
35
  import { diagnoseCodexShim, findCodexOnPath, isWindowsInteropDir, type CodexShimDiagnostic } from "../codex/shim";
36
+ import { overlayActivationHint } from "../codex/shim-overlay";
36
37
  import { providerTableString, rootTomlString } from "../codex/injected-marker";
37
38
  import { countPendingOpencodexHistory } from "../codex/history-provider";
38
39
  import {
@@ -481,28 +482,41 @@ export function collectProviderApiKeyDiagnostics(
481
482
 
482
483
  export type CodexEnvKeyReadinessDiagnostic = {
483
484
  envName: string;
484
- shimState: "missing" | "unhealthy";
485
+ shimState: "missing" | "unhealthy" | "inactive";
485
486
  detail: string;
486
487
  action: string;
487
488
  };
488
489
 
490
+ /** Shell activation and legacy migration are independent of admission-token readiness. */
491
+ export function formatCodexShimDoctorLines(shim: CodexShimDiagnostic, platform = process.platform): string[] {
492
+ if (platform === "win32" || !shim.installed) return [];
493
+ const inactiveOverlay = shim.runnable === true && shim.active === false;
494
+ const healthyLegacy = shim.healthy && shim.runnable === undefined;
495
+ return inactiveOverlay || healthyLegacy ? shim.summary.split("\n").map(line => ` ${line}`) : [];
496
+ }
497
+
489
498
  /** Warn when routed Codex cannot obtain its configured admission token at launch. */
490
499
  export function collectCodexEnvKeyReadiness(
491
500
  configText: string | null,
492
501
  env: EnvMap,
493
502
  shim: CodexShimDiagnostic,
494
503
  serviceTokenPresent: boolean,
504
+ shimActivationShown = false,
495
505
  ): CodexEnvKeyReadinessDiagnostic | null {
496
506
  if (!configText || rootTomlString(configText, "model_provider") !== "opencodex") return null;
497
507
  const envName = providerTableString(configText, "opencodex", "env_key")?.trim();
498
508
  const envValue = envName ? ownEnvValue(env, envName) : undefined;
499
509
  if (!envName || envValue?.trim() || shim.healthy || !serviceTokenPresent) return null;
500
- const shimState = shim.installed ? "unhealthy" : "missing";
510
+ const shimState = shim.runnable && shim.active === false ? "inactive" : shim.installed ? "unhealthy" : "missing";
501
511
  return {
502
512
  envName,
503
513
  shimState,
504
514
  detail: `Codex uses env_key ${envName}, but that variable is unset and the OpenCodex shim is ${shimState}; the service token file exists but plain Codex does not load it`,
505
- action: `Run 'ocx codex-shim install' to repair launch-time token injection, or export ${envName} in the process that starts Codex`,
515
+ action: shimState === "inactive"
516
+ ? shimActivationShown
517
+ ? `Activate the PATH shim as shown under Codex restart safety, or export ${envName} in the process that starts Codex`
518
+ : `${overlayActivationHint()} Or export ${envName} in the process that starts Codex`
519
+ : `Run 'ocx codex-shim install' to repair launch-time token injection, or export ${envName} in the process that starts Codex`,
506
520
  };
507
521
  }
508
522
 
@@ -1349,11 +1363,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1349
1363
  try { return readFileSync(codexConfigPath, "utf8"); } catch { return null; }
1350
1364
  })();
1351
1365
  const serviceTokenPresent = Boolean(readInstalledServiceToken()?.trim());
1366
+ const codexShim = diagnoseCodexShim();
1367
+ const codexShimLines = formatCodexShimDoctorLines(codexShim);
1352
1368
  const codexEnvKeyReadiness = collectCodexEnvKeyReadiness(
1353
1369
  codexConfigText,
1354
1370
  process.env,
1355
- diagnoseCodexShim(),
1371
+ codexShim,
1356
1372
  serviceTokenPresent,
1373
+ codexShimLines.length > 0,
1357
1374
  );
1358
1375
  // Use the same attested live startup verdict as `ocx status` when the proxy is already
1359
1376
  // identity-verified. A shell-local systemd probe can be a false negative for a system-wide
@@ -1366,6 +1383,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1366
1383
  console.log("\nCodex restart safety");
1367
1384
  console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
1368
1385
  console.log(` ${formatStartupRoutingDetail(startup)}`);
1386
+ for (const line of codexShimLines) console.log(line);
1369
1387
 
1370
1388
  console.log("\nCodex runtime selection");
1371
1389
  {
@@ -2,8 +2,8 @@ import { CAPABILITIES, type Capability } from "./capabilities";
2
2
  import { CLI_COMMANDS, findCommand, type CliCommandEntry } from "./registry";
3
3
 
4
4
  export type HelpResolution =
5
- | { kind: "entry"; entry: CliCommandEntry; canonicalName: string; children: readonly Capability[] }
6
- | { kind: "capability"; path: string[]; capability: Capability }
5
+ | { kind: "entry"; entry: CliCommandEntry; canonicalName: string; capability?: Capability; children: readonly Capability[] }
6
+ | { kind: "capability"; path: string[]; capability: Capability; children: readonly Capability[] }
7
7
  | { kind: "prefix"; path: string[]; children: readonly Capability[] }
8
8
  | { kind: "models-context"; path: string[] }
9
9
  | { kind: "unavailable"; path: string[]; parent?: string[] };
@@ -22,6 +22,18 @@ function canonicalEntry(entry: CliCommandEntry): CliCommandEntry {
22
22
  return CLI_COMMANDS.find(candidate => candidate.aliases?.includes(entry.name)) ?? entry;
23
23
  }
24
24
 
25
+ /** Help-only aliases from access dispatch; never rewrite execution argv or operands. */
26
+ function canonicalHelpPath(requested: readonly string[], entry: CliCommandEntry): string[] {
27
+ const path = entry.name === "api-key"
28
+ ? ["access", "key", ...requested.slice(1)]
29
+ : [canonicalEntry(entry).name, ...requested.slice(1)];
30
+ if (path[0] === "access") {
31
+ if (path[1] === "keys") path[1] = "key";
32
+ if (path[1] === "key" && path[2] === "delete") path[2] = "remove";
33
+ }
34
+ return path;
35
+ }
36
+
25
37
  export interface HelpRecoveryCandidate {
26
38
  names: string[];
27
39
  path: string[];
@@ -35,19 +47,24 @@ export function helpRecoveryCandidates(parent: readonly string[] = []): HelpReco
35
47
  if (entry.hidden) continue;
36
48
  const canonical = canonicalEntry(entry);
37
49
  if (canonical.hidden) continue;
38
- const candidate = candidates.get(canonical.name) ?? { names: [], path: [canonical.name] };
50
+ const path = canonicalHelpPath([entry.name], entry);
51
+ const identity = path.join(" ");
52
+ const candidate = candidates.get(identity) ?? { names: [], path };
39
53
  candidate.names = [...new Set([...candidate.names, entry.name, ...(entry.aliases ?? [])])];
40
- candidates.set(canonical.name, candidate);
54
+ candidates.set(identity, candidate);
41
55
  }
42
56
  } else {
43
- if (parent.length >= MAX_DOCUMENTED_DEPTH) return [];
44
57
  const entry = findCommand(parent[0]);
45
58
  if (!entry || entry.hidden) return [];
46
- const prefix = [canonicalEntry(entry).name, ...parent.slice(1)];
59
+ const prefix = canonicalHelpPath(parent, entry);
60
+ if (prefix.length >= MAX_DOCUMENTED_DEPTH) return [];
47
61
  for (const documented of DOCUMENTED_PATHS) {
48
62
  if (documented.length <= prefix.length || !startsWithPath(documented, prefix)) continue;
49
63
  const path = documented.slice(0, prefix.length + 1);
50
- candidates.set(path.join(" "), { names: [path[path.length - 1]], path });
64
+ const names = [path[path.length - 1]];
65
+ if (path.join(" ") === "access key") names.push("keys");
66
+ if (path.join(" ") === "access key remove") names.push("delete");
67
+ candidates.set(path.join(" "), { names, path });
51
68
  }
52
69
  }
53
70
  return [...candidates.values()];
@@ -58,19 +75,19 @@ export function resolveHelpPath(requested: readonly string[]): HelpResolution {
58
75
  const entry = requested[0] ? findCommand(requested[0]) : undefined;
59
76
  if (!entry) return { kind: "unavailable", path: [...requested] };
60
77
  // Keep exact-name alias entries for root help; nested topics use the owner.
61
- const canonical = canonicalEntry(entry);
78
+ const path = canonicalHelpPath(requested, entry);
62
79
  const declared = CAPABILITIES.filter(capability => !findCommand(capability.command[0])?.hidden);
63
80
  if (requested.length === 1) return {
64
- kind: "entry", entry, canonicalName: canonical.name,
65
- children: entry.hidden ? [] : declared.filter(candidate => candidate.command.length > 1 && candidate.command[0] === canonical.name),
81
+ kind: "entry", entry, canonicalName: path.join(" "),
82
+ capability: entry.hidden ? undefined : declared.find(candidate => candidate.command.length === path.length && startsWithPath(candidate.command, path)),
83
+ children: entry.hidden ? [] : declared.filter(candidate => candidate.command.length > path.length && startsWithPath(candidate.command, path)),
66
84
  };
67
- const path = [canonical.name, ...requested.slice(1)];
68
85
  if (path.length === 2 && path[0] === "models" && path[1] === "context") {
69
86
  return { kind: "models-context", path };
70
87
  }
71
88
  const capability = declared.find(candidate => candidate.command.length === path.length && startsWithPath(candidate.command, path));
72
- if (capability) return { kind: "capability", path, capability };
73
- const children = declared.filter(candidate => startsWithPath(candidate.command, path));
89
+ const children = declared.filter(candidate => candidate.command.length > path.length && startsWithPath(candidate.command, path));
90
+ if (capability) return { kind: "capability", path, capability, children };
74
91
  if (children.length) return { kind: "prefix", path, children };
75
92
 
76
93
  for (let length = Math.min(path.length - 1, MAX_DOCUMENTED_DEPTH); length > 1; length--) {
@@ -80,5 +97,5 @@ export function resolveHelpPath(requested: readonly string[]): HelpResolution {
80
97
  return { kind: "unavailable", path, parent };
81
98
  }
82
99
  }
83
- return { kind: "unavailable", path, parent: [canonical.name] };
100
+ return { kind: "unavailable", path, parent: [path[0]] };
84
101
  }
@@ -35,9 +35,9 @@ function distance(left: string, right: string, limit: number): number {
35
35
  export function suggestHelpPaths(path: readonly string[]): string[][] {
36
36
  if (!path.length || path.length > MAX_PATH_DEPTH || !path.every(safeToken)) return [];
37
37
  const result = resolveHelpPath(path);
38
- if (result.kind !== "unavailable") return [];
38
+ if (result.kind !== "unavailable" || result.path.length > MAX_PATH_DEPTH) return [];
39
39
  const parent = result.parent ?? [];
40
- const token = path[parent.length].toLowerCase();
40
+ const token = result.path[parent.length].toLowerCase();
41
41
  const limit = token.length < 6 ? 1 : 2;
42
42
  return helpRecoveryCandidates(parent).map(candidate => ({
43
43
  path: candidate.path,
package/src/cli/help.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolveHelpPath } from "./help-catalog";
2
+ import type { Capability } from "./capabilities";
2
3
  import { MODELS_CONTEXT_DETAILS, MODELS_CONTEXT_USAGE } from "./help-models-context";
3
4
  import { renderRootHelp } from "./help-navigation";
4
5
  import { formatHelpRecovery } from "./help-recovery";
@@ -40,7 +41,7 @@ Usage:
40
41
  ocx recover-history --ocx-compaction <thread-id> --yes
41
42
  Back up and make one ocx1-compacted thread replayable by native Codex
42
43
  ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
43
- ocx service [sub] Run as a background service (default: install/update/start)
44
+ ocx service [sub] Run as a background service (default: install if absent, otherwise repair)
44
45
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
45
46
  ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
46
47
  ocx ensure Ensure the proxy is running and Codex config/cache are current
@@ -94,9 +95,9 @@ Usage:
94
95
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
95
96
  ocx grok <sub> Grok Build model selection and apply
96
97
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
97
- ocx config <sub> Validated configuration show/get/set/import/export
98
+ ocx config [sub] Validated configuration show/get/set/import/export
98
99
  ocx companion <show|set|reset> Menu-bar and widget companion usage settings
99
- ocx lab <sub> Read-only Compatibility Lab projection inspection
100
+ ocx lab <sub> Inspect Lab evidence and control local automation
100
101
  ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
101
102
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
102
103
  ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
@@ -122,6 +123,17 @@ export function hasHelpFlag(values: string[]): boolean {
122
123
  return values.some(value => value === "--help" || value === "-h" || value === "help");
123
124
  }
124
125
 
126
+ function printCapabilityDetails(capability: Capability, write: (text: string) => void, shown: readonly string[] = []): void {
127
+ if (capability.flags.length) {
128
+ write("\nDeclared flags:");
129
+ for (const flag of capability.flags) {
130
+ write(` ${flag.name}${flag.value && flag.value !== "boolean" ? ` <${flag.value}>` : ""}${flag.required ? " (required)" : ""} ${flag.summary}`);
131
+ }
132
+ }
133
+ const details = capability.details?.filter(detail => !shown.includes(detail));
134
+ if (details?.length) write(`\n${details.join("\n")}`);
135
+ }
136
+
125
137
  export function printSubcommandUsage(
126
138
  name: string | undefined,
127
139
  path?: readonly string[],
@@ -142,6 +154,7 @@ export function printSubcommandUsage(
142
154
  if (result.kind === "entry") {
143
155
  write(`Usage: ${result.entry.usage}\n\n${result.entry.summary}`);
144
156
  if (result.entry.details?.length) write(`\n${result.entry.details.join("\n")}`);
157
+ if (result.capability) printCapabilityDetails(result.capability, write, result.entry.details);
145
158
  if (result.children.length) {
146
159
  write("\nDeclared commands (incomplete):");
147
160
  for (const child of result.children) write(` ocx help ${child.command.join(" ")} ${child.summary}`);
@@ -154,15 +167,16 @@ export function printSubcommandUsage(
154
167
  write(`Usage:\n${MODELS_CONTEXT_USAGE}\n\n${MODELS_CONTEXT_DETAILS.join("\n")}`);
155
168
  } else if (result.kind === "capability") {
156
169
  const { capability } = result;
157
- write(`Command: ocx ${result.path.join(" ")}\n\n${capability.summary}`);
158
- if (capability.flags.length) {
159
- write("\nDeclared flags:");
160
- for (const flag of capability.flags) {
161
- write(` ${flag.name}${flag.value && flag.value !== "boolean" ? ` <${flag.value}>` : ""}${flag.required ? " (required)" : ""} ${flag.summary}`);
162
- }
170
+ const heading = capability.usage !== undefined ? `Usage: ${capability.usage}` : `Command: ocx ${result.path.join(" ")}`;
171
+ write(`${heading}\n\n${capability.summary}`);
172
+ printCapabilityDetails(capability, write);
173
+ if (result.children.length) {
174
+ write("\nDeclared commands (incomplete):");
175
+ for (const child of result.children) write(` ocx help ${child.command.join(" ")} ${child.summary}`);
176
+ }
177
+ if (capability.usage === undefined) {
178
+ write("\nCapability metadata is incomplete; this is not the full operand grammar.");
163
179
  }
164
- if (capability.details?.length) write(`\n${capability.details.join("\n")}`);
165
- write("\nCapability metadata is incomplete; this is not the full operand grammar.");
166
180
  } else {
167
181
  write(`Command group: ocx ${result.path.join(" ")}\n\nDeclared commands (incomplete):`);
168
182
  for (const child of result.children) write(` ocx ${child.command.join(" ")} ${child.summary}`);