@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.1

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 (94) hide show
  1. package/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
  2. package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
  3. package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
  4. package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
  5. package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
  6. package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
  7. package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
  8. package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
  9. package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
  10. package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
  11. package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
  12. package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
  13. package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
  14. package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
  15. package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
  16. package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
  17. package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
  18. package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
  19. package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
  20. package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
  21. package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
  22. package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
  23. package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
  24. package/gui/dist/assets/shared-DI18-uF7.js +69 -0
  25. package/gui/dist/index.html +2 -2
  26. package/package.json +3 -1
  27. package/src/adapters/base.ts +26 -0
  28. package/src/adapters/cursor/catalog.ts +541 -0
  29. package/src/adapters/cursor/cursor-errors.ts +15 -0
  30. package/src/adapters/cursor/discovery.ts +34 -41
  31. package/src/adapters/cursor/envelope-echo.ts +128 -0
  32. package/src/adapters/cursor/request-builder.ts +19 -12
  33. package/src/adapters/cursor/tool-definitions.ts +2 -1
  34. package/src/adapters/cursor/tool-result-normalize.ts +23 -31
  35. package/src/adapters/cursor.ts +21 -2
  36. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  37. package/src/adapters/google-antigravity-replay.ts +71 -2
  38. package/src/adapters/google.ts +19 -4
  39. package/src/adapters/kiro-constants.ts +12 -0
  40. package/src/adapters/kiro.ts +128 -11
  41. package/src/adapters/openai-chat.ts +16 -2
  42. package/src/adapters/openai-responses.ts +15 -2
  43. package/src/adapters/tool-catalog-nudge.ts +2 -1
  44. package/src/adapters/xai-web-search.ts +10 -14
  45. package/src/claude/outbound.ts +14 -3
  46. package/src/cli/access.ts +46 -3
  47. package/src/cli/account-api.ts +93 -16
  48. package/src/cli/account-extended.ts +262 -36
  49. package/src/cli/account-main.ts +12 -12
  50. package/src/cli/account.ts +40 -10
  51. package/src/cli/agent.ts +8 -1
  52. package/src/cli/capabilities-command.ts +94 -0
  53. package/src/cli/capabilities.ts +535 -0
  54. package/src/cli/claude-desktop.ts +31 -11
  55. package/src/cli/dispatch.ts +195 -27
  56. package/src/cli/doctor.ts +100 -1
  57. package/src/cli/help.ts +11 -2
  58. package/src/cli/index.ts +19 -3
  59. package/src/cli/inspect.ts +230 -0
  60. package/src/cli/observe.ts +11 -3
  61. package/src/cli/registry.ts +34 -2
  62. package/src/cli/runtime-api.ts +51 -7
  63. package/src/cli/status.ts +16 -0
  64. package/src/cli/storage.ts +234 -0
  65. package/src/cli/system-command.ts +16 -0
  66. package/src/cli/usage-report.ts +52 -2
  67. package/src/cli/version-skew.ts +46 -0
  68. package/src/codex/account-label.ts +21 -0
  69. package/src/codex/catalog/provider-fetch.ts +4 -0
  70. package/src/codex/transition-state.ts +12 -3
  71. package/src/compatibility/openai-responses.ts +9 -1
  72. package/src/generated/compatibility-version.json +94 -58
  73. package/src/integrations/ownership-policy.ts +24 -5
  74. package/src/integrations/ownership.ts +36 -2
  75. package/src/integrations/state.ts +40 -7
  76. package/src/integrations/writer.ts +21 -3
  77. package/src/lib/admin-secrets.ts +24 -0
  78. package/src/lib/errors.ts +25 -1
  79. package/src/lib/service-secrets.ts +15 -0
  80. package/src/oauth/store.ts +16 -6
  81. package/src/providers/label.ts +34 -1
  82. package/src/responses/turn-termination.ts +107 -0
  83. package/src/server/management/logs-usage-routes.ts +5 -27
  84. package/src/server/management/route-registry.ts +317 -0
  85. package/src/server/proxy-liveness.ts +27 -4
  86. package/src/server/request-log.ts +37 -17
  87. package/src/server/responses/core.ts +80 -4
  88. package/src/server/responses/policy-fallback.ts +1 -1
  89. package/src/service.ts +34 -0
  90. package/src/storage/policy-job.ts +14 -4
  91. package/src/storage/policy.ts +79 -33
  92. package/src/usage/log.ts +32 -6
  93. package/src/usage/summary.ts +22 -34
  94. package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
@@ -0,0 +1,230 @@
1
+ /**
2
+ * `ocx system-surface` — the remaining routes that had no CLI caller at all (wp7).
3
+ *
4
+ * These are grouped by what an operator is trying to do, not by which server module owns them:
5
+ * inspecting effective configuration, reading the generated client-config snippet, toggling the
6
+ * native client integrations, checking request pacing and routing analytics, and reading the
7
+ * Codex system prompt.
8
+ *
9
+ * One route here is deliberately READ-ONLY forever. `POST /api/github/star` spends the operator's
10
+ * GitHub identity, and no CLI flag can carry that consent — the server requires a real dashboard
11
+ * session for it. So `star` reads status and says what it cannot do, rather than offering a
12
+ * `--yes` that would be a lie.
13
+ */
14
+ import {
15
+ CliUsageError,
16
+ printData,
17
+ rejectArgs,
18
+ runCliAction,
19
+ runtimeRequest,
20
+ summaryLines,
21
+ takeFlag,
22
+ takeOption,
23
+ type RuntimeApiDeps,
24
+ } from "./runtime-api";
25
+
26
+ const USAGE = `Usage:
27
+ ocx inspect config [--json]
28
+ ocx inspect catalog [--json]
29
+ ocx inspect routing-analytics [--json]
30
+ ocx inspect pacing [--name <provider>] [--json]
31
+ ocx inspect key-providers [--json]
32
+ ocx inspect codex-prompt [--text] [--json]
33
+ ocx inspect client-config --client <id> [--json]
34
+ ocx inspect star [--json]
35
+ ocx inspect windows-tray [--json]
36
+ ocx integration native [list] [--json]
37
+ ocx integration native <claude|claude-desktop|codex|grok> <on|off> [--json]
38
+ ocx agent request-user-input [on|off] [--json]`;
39
+
40
+ /** A read that takes no arguments beyond `--json`. */
41
+ async function read(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
42
+ const args = [...argv];
43
+ const wantsJson = takeFlag(args, "--json");
44
+ rejectArgs(args, USAGE);
45
+ const result = await runtimeRequest(path, {}, deps);
46
+ printData(result, wantsJson, summaryLines(result));
47
+ }
48
+
49
+ const NATIVE_CLIENTS = ["claude", "claude-desktop", "codex", "grok"] as const;
50
+
51
+ interface NativeClientRow {
52
+ clientId?: string;
53
+ state?: string;
54
+ installed?: boolean;
55
+ desiredEnabled?: boolean;
56
+ configPath?: string;
57
+ disableBlocked?: unknown;
58
+ }
59
+
60
+ /**
61
+ * The shared depth-1 flattener renders an array as "N item(s)", so a bare
62
+ * `integration native list` printed `clients: 4 item(s)` -- the per-client state the operator
63
+ * asked for was in the payload and discarded before the terminal. Same defect class wp4 fixed
64
+ * for the account and key tables.
65
+ */
66
+ function nativeLines(payload: unknown): string[] {
67
+ const clients = (payload as { clients?: NativeClientRow[] } | null)?.clients;
68
+ if (!Array.isArray(clients) || clients.length === 0) return ["No native client integrations reported."];
69
+ const lines = ["CLIENT STATE INSTALLED DESIRED CONFIG"];
70
+ for (const row of clients) {
71
+ lines.push([
72
+ (row.clientId ?? "?").padEnd(16),
73
+ (row.state ?? "?").padEnd(10),
74
+ (row.installed === true ? "yes" : row.installed === false ? "no" : "?").padEnd(10),
75
+ (row.desiredEnabled === true ? "on" : row.desiredEnabled === false ? "off" : "?").padEnd(8),
76
+ row.configPath ?? "",
77
+ ].join(" ").trimEnd());
78
+ // A blocked disable is the reason a toggle did not take effect, so it is never silent.
79
+ if (row.disableBlocked !== null && row.disableBlocked !== undefined) {
80
+ lines.push(` disable blocked: ${typeof row.disableBlocked === "string" ? row.disableBlocked : JSON.stringify(row.disableBlocked)}`);
81
+ }
82
+ }
83
+ return lines;
84
+ }
85
+
86
+ async function nativeIntegration(argv: string[], deps: RuntimeApiDeps): Promise<void> {
87
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
88
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
89
+
90
+ if (action === "list") {
91
+ const args = [...rest];
92
+ const wantsJson = takeFlag(args, "--json");
93
+ rejectArgs(args, USAGE);
94
+ const result = await runtimeRequest("/api/native-integrations", {}, deps);
95
+ printData(result, wantsJson, nativeLines(result));
96
+ return;
97
+ }
98
+
99
+ if (!(NATIVE_CLIENTS as readonly string[]).includes(action)) {
100
+ throw new CliUsageError(`unknown native client ${action}; expected one of ${NATIVE_CLIENTS.join(", ")}`, USAGE);
101
+ }
102
+
103
+ const args = [...rest];
104
+ const wantsJson = takeFlag(args, "--json");
105
+ const state = args.shift();
106
+ rejectArgs(args, USAGE);
107
+ if (state !== "on" && state !== "off") {
108
+ throw new CliUsageError(`expected on or off after ${action}`, USAGE);
109
+ }
110
+
111
+ // Toggling rewrites the client's own config file, which is why each client has its own route
112
+ // rather than one route with a client parameter.
113
+ const result = await runtimeRequest(`/api/native-integrations/${action}`, {
114
+ method: "PUT",
115
+ headers: { "content-type": "application/json" },
116
+ body: JSON.stringify({ enabled: state === "on" }),
117
+ }, deps);
118
+ printData(result, wantsJson, summaryLines(result));
119
+ }
120
+
121
+ /**
122
+ * Exported unwrapped so `ocx agent request-user-input` can call it INSIDE its own
123
+ * `runCliAction`. Wrapping an already-wrapped handler reports one failure twice: the inner
124
+ * wrapper prints the error and returns a code, and the outer one prints again on the rethrow.
125
+ */
126
+ export async function requestUserInputAction(argv: string[], deps: RuntimeApiDeps): Promise<void> {
127
+ const args = [...argv];
128
+ const wantsJson = takeFlag(args, "--json");
129
+ const state = args.shift();
130
+ rejectArgs(args, USAGE);
131
+
132
+ const path = "/api/codex-auth/features/default-mode-request-user-input";
133
+ // No argument means show. A read must not write the value it is reporting.
134
+ if (state === undefined) {
135
+ const result = await runtimeRequest(path, {}, deps);
136
+ printData(result, wantsJson, summaryLines(result));
137
+ return;
138
+ }
139
+ if (state !== "on" && state !== "off") throw new CliUsageError("expected on or off", USAGE);
140
+
141
+ const result = await runtimeRequest(path, {
142
+ method: "PUT",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify({ enabled: state === "on" }),
145
+ }, deps);
146
+ printData(result, wantsJson, summaryLines(result));
147
+ }
148
+
149
+ async function clientConfig(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
+ const args = [...argv];
151
+ const wantsJson = takeFlag(args, "--json");
152
+ const client = takeOption(args, "--client");
153
+ rejectArgs(args, USAGE);
154
+ if (!client) throw new CliUsageError("--client is required", USAGE);
155
+ // The valid client list is not duplicated here: the route answers 400 naming every accepted id,
156
+ // which is more useful than a local list that can drift out of date.
157
+ const result = await runtimeRequest(`/api/client-config?client=${encodeURIComponent(client)}`, {}, deps);
158
+ printData(result, wantsJson, summaryLines(result));
159
+ }
160
+
161
+ async function pacing(argv: string[], deps: RuntimeApiDeps): Promise<void> {
162
+ const args = [...argv];
163
+ const wantsJson = takeFlag(args, "--json");
164
+ const name = takeOption(args, "--name");
165
+ rejectArgs(args, USAGE);
166
+ const suffix = name ? `?name=${encodeURIComponent(name)}` : "";
167
+ const result = await runtimeRequest(`/api/provider-request-pacing${suffix}`, {}, deps);
168
+ printData(result, wantsJson, summaryLines(result));
169
+ }
170
+
171
+ async function codexPrompt(argv: string[], deps: RuntimeApiDeps): Promise<void> {
172
+ const args = [...argv];
173
+ const wantsJson = takeFlag(args, "--json");
174
+ const asText = takeFlag(args, "--text");
175
+ rejectArgs(args, USAGE);
176
+ if (asText && wantsJson) throw new CliUsageError("--text and --json cannot be combined", USAGE);
177
+ if (asText) {
178
+ // The /text variant answers the prompt body itself, so it is printed verbatim rather than
179
+ // flattened through the summary renderer.
180
+ const result = await runtimeRequest<unknown>("/api/codex-prompt/text", {}, deps);
181
+ console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
182
+ return;
183
+ }
184
+ const result = await runtimeRequest("/api/codex-prompt", {}, deps);
185
+ printData(result, wantsJson, summaryLines(result));
186
+ }
187
+
188
+ async function star(argv: string[], deps: RuntimeApiDeps): Promise<void> {
189
+ const args = [...argv];
190
+ const wantsJson = takeFlag(args, "--json");
191
+ rejectArgs(args, USAGE);
192
+ const result = await runtimeRequest<Record<string, unknown>>("/api/github/star", {}, deps);
193
+ if (wantsJson) {
194
+ printData(result, true);
195
+ return;
196
+ }
197
+ printData(result, false, summaryLines(result));
198
+ // Said plainly, because the natural next question is "then star it", and the answer is that no
199
+ // CLI invocation can: the POST requires a dashboard session precisely so an agent cannot spend
200
+ // the operator's identity on their behalf.
201
+ console.log("Starring is not available from the CLI: it uses your GitHub identity, so only you can do it from the dashboard.");
202
+ }
203
+
204
+ export async function handleInspectCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
205
+ return runCliAction(async () => {
206
+ const hasSub = argv[0] !== undefined && !argv[0].startsWith("-");
207
+ const sub = hasSub ? argv[0]! : "config";
208
+ const rest = hasSub ? argv.slice(1) : argv;
209
+ if (sub === "config") await read("/api/config", rest, deps);
210
+ else if (sub === "catalog") await read("/api/catalog", rest, deps);
211
+ else if (sub === "routing-analytics") await read("/api/routing-analytics", rest, deps);
212
+ else if (sub === "key-providers") await read("/api/key-providers", rest, deps);
213
+ else if (sub === "windows-tray") await read("/api/windows-tray", rest, deps);
214
+ else if (sub === "pacing") await pacing(rest, deps);
215
+ else if (sub === "client-config") await clientConfig(rest, deps);
216
+ else if (sub === "codex-prompt") await codexPrompt(rest, deps);
217
+ else if (sub === "star") await star(rest, deps);
218
+ else throw new CliUsageError(`unknown inspect command ${sub}`, USAGE);
219
+ });
220
+ }
221
+
222
+ export async function handleIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
223
+ return runCliAction(async () => {
224
+ const [sub, ...rest] = argv;
225
+ if (sub === "native" || sub === undefined) await nativeIntegration(sub === undefined ? [] : rest, deps);
226
+ else throw new CliUsageError(`unknown integration command ${sub}`, USAGE);
227
+ });
228
+ }
229
+
230
+ export const INSPECT_USAGE = USAGE;
@@ -15,7 +15,7 @@ import { USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
15
15
 
16
16
  const USAGE = `Usage:
17
17
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
18
- [--limit <n>] [--follow] [--json|--jsonl]
18
+ [--conversation <id>] [--limit <n>] [--follow] [--json|--jsonl]
19
19
  ocx logs explain <request-id> [--json]
20
20
  ocx logs rebuild-index
21
21
  ocx logs index-status
@@ -50,7 +50,12 @@ function formatLog(row: LogEntry): string {
50
50
  const route = [row.provider, row.model].filter(Boolean).join("/");
51
51
  const status = row.status ?? row.statusCode ?? "?";
52
52
  const duration = row.durationMs !== undefined ? `${String(row.durationMs)}ms` : "";
53
- return [time, String(status), route, duration].filter(Boolean).join(" ");
53
+ // The conversation id is shown because a conversation FILTER whose output never names the
54
+ // conversation is hard to trust: an empty result and a wrong-id result look identical (#2704).
55
+ const conversation = typeof row.conversationId === "string" && row.conversationId.length > 0
56
+ ? `conv=${row.conversationId}`
57
+ : "";
58
+ return [time, String(status), route, duration, conversation].filter(Boolean).join(" ");
54
59
  }
55
60
 
56
61
  async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -61,13 +66,16 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
61
66
  const provider = takeOption(args, "--provider");
62
67
  const model = takeOption(args, "--model");
63
68
  const status = takeOption(args, "--status");
69
+ // Both spellings, because the server accepts both (`request-log.ts:1032`) and an operator
70
+ // should not have to remember which one this surface wanted.
71
+ const conversationId = takeOption(args, "--conversation") ?? takeOption(args, "--conversationId");
64
72
  const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? 200;
65
73
  rejectArgs(args, USAGE);
66
74
  if (wantsJson && wantsJsonl) throw new CliUsageError("--json and --jsonl cannot be combined", USAGE);
67
75
  if (follow && wantsJson) throw new CliUsageError("--follow uses --jsonl, not --json", USAGE);
68
76
  let seen = new Set<string>();
69
77
  do {
70
- const data = await runtimeRequest(`/api/logs${query({ provider, model, status, limit })}`, {}, deps);
78
+ const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, limit })}`, {}, deps);
71
79
  const rows = logRows(data);
72
80
  if (!follow && wantsJson) printData(data, true);
73
81
  else {
@@ -213,13 +213,29 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
213
213
  usage: "ocx observe <logs|usage|storage|memory|debug|claude-inbound|injection> ...",
214
214
  summary: "Inspect proxy requests, usage, storage, memory, and debug data.",
215
215
  },
216
+ {
217
+ name: "inspect",
218
+ usage: "ocx inspect <config|catalog|routing-analytics|pacing|key-providers|codex-prompt|client-config|star|windows-tray> ...",
219
+ summary: "Read effective config, catalog, analytics, pacing, and the generated client-config snippet.",
220
+ details: [
221
+ "`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
222
+ ],
223
+ },
216
224
  { name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
217
225
  {
218
226
  name: "usage",
219
227
  usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
220
228
  summary: "Alias of ocx observe usage.",
221
229
  },
222
- { name: "storage", usage: "ocx storage [--json]", summary: "Alias of ocx observe storage." },
230
+ {
231
+ name: "storage",
232
+ usage: "ocx storage <report|cleanup|trash|policy> ...",
233
+ summary: "Storage report, archived-session cleanup, trash restore, and the cleanup policy.",
234
+ details: [
235
+ "A bare `ocx storage` prints the report, as it did when this was an alias of `observe storage`.",
236
+ "`cleanup` previews by default and only deletes under --yes; `trash restore` and `policy run` also require --yes.",
237
+ ],
238
+ },
223
239
  { name: "memory", usage: "ocx memory [--json]", summary: "Alias of ocx observe memory." },
224
240
  {
225
241
  name: "access",
@@ -243,7 +259,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
243
259
  usage: "ocx grok <status|exclude|include|set|clear|apply> ...",
244
260
  summary: "Manage and apply the Grok Build model fence.",
245
261
  },
246
- { name: "integration", usage: "ocx integration <claude|grok|client> ...", summary: "Manage supported client integrations." },
262
+ {
263
+ name: "integration",
264
+ usage: "ocx integration <claude|grok|client|native> ...",
265
+ summary: "Manage supported client integrations, and the native client toggles.",
266
+ details: [
267
+ "`native` shows or flips the native Claude/Claude Desktop/Codex/Grok toggles; the other subcommands manage the reversible file integrations.",
268
+ ],
269
+ },
247
270
  {
248
271
  name: "system",
249
272
  usage: "ocx system <status|settings|startup|diagnostics|sync|update> ...",
@@ -350,6 +373,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
350
373
  summary: "Check proxy health. Exits 0 if healthy, 1 otherwise.",
351
374
  details: ["Use --json for structured output: {ok, pid, port}."],
352
375
  },
376
+ {
377
+ name: "capabilities",
378
+ usage: "ocx capabilities [--json] [--mutating-only] [--route <path>]",
379
+ summary: "List the declared CLI capabilities and the management routes they drive.",
380
+ details: [
381
+ "The machine-readable surface index: start here when driving ocx programmatically instead of parsing help text.",
382
+ "--route <path> answers the inverse question: which commands drive this management route.",
383
+ ],
384
+ },
353
385
  {
354
386
  name: "ready",
355
387
  usage: "ocx ready [--json] [--wait [--timeout <seconds>]]",
@@ -47,15 +47,37 @@ export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string>
47
47
  return `http://${probeHostname(live.hostname)}:${live.port}`;
48
48
  }
49
49
 
50
+ function stringField(record: Record<string, unknown>, key: string): string | undefined {
51
+ const value = record[key];
52
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
53
+ }
54
+
55
+ /**
56
+ * Compose the operator-facing message from a management error body.
57
+ *
58
+ * The server states WHY a request was refused under `reason` and WHAT TO DO under
59
+ * `hint` (see management-auth.ts, which sets both on a 503 when the management plane
60
+ * is unavailable). Both were dropped here, so a fenced management plane was
61
+ * indistinguishable from a generic failure and an operator had no way to tell a port
62
+ * collision from an ACL refusal from a stopped proxy (#2698).
63
+ */
50
64
  function responseMessage(body: unknown, status: number): string {
51
- if (body && typeof body === "object") {
52
- const record = body as Record<string, unknown>;
53
- for (const key of ["error", "message", "detail"]) {
54
- if (typeof record[key] === "string" && record[key]) return record[key];
55
- }
56
- }
57
65
  if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
58
- return `Management request failed (${status})`;
66
+ if (!body || typeof body !== "object") return `Management request failed (${status})`;
67
+ const record = body as Record<string, unknown>;
68
+ let primary: string | undefined;
69
+ for (const key of ["error", "message", "detail"]) {
70
+ primary = stringField(record, key);
71
+ if (primary) break;
72
+ }
73
+ const parts = [primary ?? `Management request failed (${status})`];
74
+ const reason = stringField(record, "reason");
75
+ // A body of {ok:false, reason:"…"} with no `error` key used to degrade to the
76
+ // generic line, discarding the only actionable field.
77
+ if (reason && reason !== primary) parts.push(`reason: ${reason}`);
78
+ const hint = stringField(record, "hint");
79
+ if (hint && hint !== primary) parts.push(`hint: ${hint}`);
80
+ return parts.join("\n").slice(0, 1200);
59
81
  }
60
82
 
61
83
  export async function runtimeRequest<T = unknown>(
@@ -94,6 +116,28 @@ export function takeFlag(args: string[], flag: string): boolean {
94
116
  return true;
95
117
  }
96
118
 
119
+ /** Unicode dashes that copy-paste and IME input substitute for ASCII `-`. */
120
+ const DASH_CLASS = /[\u2010-\u2015\u2212]/g;
121
+
122
+ /**
123
+ * True for `--json`, `--json=true`, `-json`, and Unicode-dash spellings.
124
+ * Matching only the exact token `--json` is the same defect logout had: each
125
+ * spelling that slips through is a silent success (or, for doctor, prose on
126
+ * stdout after the caller asked for JSON).
127
+ */
128
+ export function isJsonOption(arg: string): boolean {
129
+ const body = arg.replace(DASH_CLASS, "-").replace(/^-+/, "");
130
+ return body === "json" || body.startsWith("json=");
131
+ }
132
+
133
+ /** Remove one JSON-request spelling from `args`. Returns whether one was present. */
134
+ export function takeJsonFlag(args: string[]): boolean {
135
+ const index = args.findIndex(isJsonOption);
136
+ if (index === -1) return false;
137
+ args.splice(index, 1);
138
+ return true;
139
+ }
140
+
97
141
  export function takeOption(args: string[], flag: string): string | undefined {
98
142
  const index = args.indexOf(flag);
99
143
  if (index === -1) return undefined;
package/src/cli/status.ts CHANGED
@@ -10,6 +10,8 @@ import { collectStartupHealth, type StartupHealth } from "../codex/autostart-hea
10
10
  import { getCodexRoutingKind } from "../codex/inject";
11
11
  import { diagnoseCodexShim } from "../codex/shim";
12
12
  import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime";
13
+ import { packageVersion } from "./help";
14
+ import { computeVersionSkew, type VersionSkew } from "./version-skew";
13
15
  import { redactSecretString, redactUserPath } from "../lib/redact";
14
16
  import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home";
15
17
  import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
@@ -70,6 +72,13 @@ export type CliStatusJson = {
70
72
  };
71
73
  };
72
74
  codexHome: OrcaCodexHomeDiagnostic;
75
+ /**
76
+ * This CLI's version against the running proxy's (#2701).
77
+ *
78
+ * Additive and optional-by-value, so `schemaVersion` stays 1: an existing consumer that
79
+ * ignores the key is unaffected, and `proxyVersion` is null when nothing is live.
80
+ */
81
+ versionSkew: VersionSkew;
73
82
  };
74
83
 
75
84
  export type CliStatusView = {
@@ -180,6 +189,9 @@ export async function collectStatus(): Promise<CliStatusView> {
180
189
  const pidFile = readPid();
181
190
  // Preserve an authoritative null from orphan/legacy liveness — do not restore pidFile.
182
191
  const pid = resolveStatusPid(live, pidFile);
192
+ // No extra request: findLiveProxy's identity probe already parsed and validated the
193
+ // healthz body, so the version came back with the liveness result.
194
+ const versionSkew = computeVersionSkew(packageVersion(), live?.version);
183
195
  const listen = live
184
196
  ? {
185
197
  port: live.port,
@@ -347,6 +359,10 @@ export async function collectStatus(): Promise<CliStatusView> {
347
359
  codexPlugins,
348
360
  codexRuntime,
349
361
  codexHome,
362
+ // Own field rather than a line in `codexRuntime.warning`: a stale ocx on PATH is a
363
+ // fact about this install, not about the Codex runtime, and filing it there would
364
+ // print it under the wrong heading (#2701).
365
+ versionSkew,
350
366
  },
351
367
  };
352
368
  }