@bitkyc08/opencodex 2.52.0 → 2.53.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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -41,6 +41,7 @@ const REPLACEMENT_STYLE_OAUTH = new Set<string>();
41
41
 
42
42
  const ACCOUNT_USAGE = `Usage:
43
43
  ocx account list [provider] [--json] [--all] [--quota [--refresh]]
44
+ ocx account history openai <pool-account-id> [--limit <1-200>] [--json]
44
45
  ocx account current <provider> [--json]
45
46
  ocx account use <provider> <account-or-key-id|main> [--json]
46
47
  ocx account refresh <provider> [--json]
@@ -50,7 +51,7 @@ const ACCOUNT_USAGE = `Usage:
50
51
  ocx account pause <provider> <account-id|main> [--json]
51
52
  ocx account resume <provider> <account-id|main> [--json]
52
53
  ocx account pause-exhausted <provider> [--json]
53
- ocx account strategy <provider> [<quota|round-robin|fill-first>] [--json]
54
+ ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
54
55
  ocx account sticky <provider> [<1-100>] [--json]
55
56
  ocx account remove <provider> <account-or-key-id|main> --yes [--json]
56
57
  ocx account clear-cooldown <provider> <account-id|main> [--json]
@@ -61,7 +62,7 @@ const ACCOUNT_USAGE = `Usage:
61
62
  ocx account cancel <provider> [--flow <flow-id>] [--json]
62
63
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
63
64
  ocx account grok-reset-coupons [<account-id>] [--consume --yes] [--token-id <token-id>] [--json]
64
- ocx account main <doctor|list|register|add|switch|recover> ...
65
+ ocx account main <doctor|list|register|add|reauth|switch|recover> ...
65
66
 
66
67
  List and switch provider accounts and API-key pools (masked output only).
67
68
  'main' selects the Codex App login for the openai account pool.`;
@@ -101,6 +102,9 @@ function statusText(row: AccountRow): string {
101
102
  if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
102
103
  if (row.needsReauth) parts.push("needs-reauth");
103
104
  if (row.validationPending) parts.push("validation-pending");
105
+ if (row.selectionExcludedReason === "plan_excluded") {
106
+ parts.push(`not-auto-selected(plan=${row.selectionExcludedPlan ?? row.plan ?? "unknown"})`);
107
+ }
104
108
  return parts.join(" ");
105
109
  }
106
110
 
@@ -115,7 +119,7 @@ function priorityText(row: AccountRow): string {
115
119
  * decides on before a long session. The full breakdown stays in `--json`.
116
120
  */
117
121
  function quotaText(row: AccountRow): string {
118
- if ((row as { quotaUnavailable?: boolean }).quotaUnavailable) return "unavailable";
122
+ if (row.quotaUnavailable) return row.quotaFailure ? `unavailable (${row.quotaFailure})` : "unavailable";
119
123
  const quota = row.quota;
120
124
  if (!quota) return "-";
121
125
  const parts: string[] = [];
@@ -335,6 +339,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
335
339
  const [sub, ...rest] = args;
336
340
  try {
337
341
  if (sub === "list") return await cmdList(rest, deps);
342
+ if (sub === "history") {
343
+ const { cmdAccountHistory } = await import("./account-history");
344
+ return await cmdAccountHistory(rest, deps);
345
+ }
338
346
  if (sub === "current") return await cmdCurrent(rest, deps);
339
347
  if (sub === "use") return await cmdUse(rest, deps);
340
348
  if (sub === "refresh") return await cmdRefresh(rest, deps);
@@ -95,6 +95,85 @@ export const HEAD_CAPABILITIES: readonly HeadCapability[] = [
95
95
  * A capability must not name a route the command does not actually fetch.
96
96
  */
97
97
  export const CAPABILITIES: readonly Capability[] = [
98
+ {
99
+ "command": [
100
+ "remote-workspace",
101
+ "pair"
102
+ ],
103
+ "summary": "Enroll this executor with one Hub using a one-time code from stdin and locally approved roots.",
104
+ "routes": [],
105
+ "flags": [
106
+ {
107
+ "name": "--json",
108
+ "value": "boolean",
109
+ "summary": "Emit the public local executor status."
110
+ },
111
+ {
112
+ "name": "--pairing-code-stdin",
113
+ "value": "boolean",
114
+ "summary": "Read the one-time pairing code from stdin."
115
+ },
116
+ {
117
+ "name": "--root",
118
+ "value": "string",
119
+ "summary": "Approve an absolute workspace directory; repeatable."
120
+ },
121
+ {
122
+ "name": "--toolchain-root",
123
+ "value": "string",
124
+ "summary": "Approve a read-only toolchain directory; repeatable."
125
+ },
126
+ {
127
+ "name": "--executor-helper",
128
+ "value": "string",
129
+ "summary": "Select a reviewed native helper file."
130
+ },
131
+ {
132
+ "name": "--name",
133
+ "value": "string",
134
+ "summary": "Name this executor."
135
+ }
136
+ ],
137
+ "mutates": true,
138
+ "json": "payload",
139
+ "details": [
140
+ "Executor-local operation; Hub consent and session control stay in the dashboard."
141
+ ]
142
+ },
143
+ {
144
+ "command": [
145
+ "remote-workspace",
146
+ "agent"
147
+ ],
148
+ "summary": "Keep the paired executor connected to its Hub.",
149
+ "routes": [],
150
+ "flags": [],
151
+ "mutates": true,
152
+ "json": "none",
153
+ "details": [
154
+ "Executor-local operation; Hub consent and session control stay in the dashboard."
155
+ ]
156
+ },
157
+ {
158
+ "command": [
159
+ "remote-workspace",
160
+ "status"
161
+ ],
162
+ "summary": "Read local executor enrollment and available capabilities without printing credentials.",
163
+ "routes": [],
164
+ "flags": [
165
+ {
166
+ "name": "--json",
167
+ "value": "boolean",
168
+ "summary": "Emit the public local executor status."
169
+ }
170
+ ],
171
+ "mutates": false,
172
+ "json": "payload",
173
+ "details": [
174
+ "Executor-local operation; Hub consent and session control stay in the dashboard."
175
+ ]
176
+ },
98
177
  {
99
178
  command: ["models", "price"],
100
179
  summary: "Read the saved manual price for an exact provider/model selector.",
@@ -235,6 +314,40 @@ export const CAPABILITIES: readonly Capability[] = [
235
314
  "Headless services usually have no unlocked keychain session; prefer ${ENV_VAR} references there.",
236
315
  ],
237
316
  },
317
+ {
318
+ command: ["account", "history"],
319
+ summary: "Cached quota observations for one stored Codex pool account.",
320
+ routes: [{ method: "GET", path: "/api/codex-auth/quota/history" }],
321
+ flags: [
322
+ { name: "--json", value: "boolean", summary: "Emit the bounded observation history." },
323
+ { name: "--limit", value: "number", summary: "Return the newest 1 to 200 observations." },
324
+ ],
325
+ mutates: false,
326
+ json: "payload",
327
+ details: ["Use account history openai <pool-account-id>. Reads cached observations only; no refresh or warmup. Native main is not included."],
328
+ },
329
+ {
330
+ command: ["account", "main", "reauth"],
331
+ summary: "Reauthenticate the native main Codex login with a device code (#3898); headless hubs need no Codex App or keyring.",
332
+ routes: [
333
+ { method: "POST", path: "/api/codex-auth/main/reauth-device" },
334
+ { method: "GET", path: "/api/codex-auth/main/reauth-device" },
335
+ { method: "DELETE", path: "/api/codex-auth/main/reauth-device" },
336
+ ],
337
+ flags: [
338
+ { name: "--device", value: "boolean", summary: "Run the device-code flow (the only reauth mode)." },
339
+ { name: "--no-wait", value: "boolean", summary: "Print the flow handle and code without waiting for completion." },
340
+ { name: "--flow", value: "string", summary: "Flow id for status and cancel." },
341
+ { name: "--json", value: "boolean", summary: "Emit the flow status as JSON." },
342
+ ],
343
+ mutates: true,
344
+ json: "payload",
345
+ details: [
346
+ "Same-identity reauth only: the device login must complete for the ChatGPT account that already holds the native main slot, and the commit is fenced by the exclusive claim plus a path/hash/inode snapshot.",
347
+ "/api/codex-auth/login stays pool-only and keeps rejecting __main__; this namespace is the only device-reauth surface for the native main slot.",
348
+ "Payloads carry only flowId, status, the verification URL, the device code, and a closed set of failure codes -- never tokens, emails, or raw account ids.",
349
+ ],
350
+ },
238
351
  {
239
352
  command: ["account", "list"],
240
353
  summary: "Codex OAuth accounts with pool priority and pause state.",
@@ -0,0 +1,109 @@
1
+ import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
2
+ import { pullRemoteCatalog, RemoteCatalogError } from "../codex/catalog/remote";
3
+ import { hasHelpFlag, printSubcommandUsage } from "./help";
4
+
5
+ export interface CatalogPullEnvelope {
6
+ schemaVersion: 1;
7
+ ok: boolean;
8
+ status: "updated" | "unchanged" | "failed";
9
+ catalogWritten: boolean;
10
+ cacheSynced: boolean;
11
+ codexRestarted: boolean;
12
+ modelCount?: number;
13
+ code?: string;
14
+ }
15
+
16
+ function optionValue(args: string[], name: string): string | undefined {
17
+ const index = args.indexOf(name);
18
+ return index >= 0 ? args[index + 1] : undefined;
19
+ }
20
+
21
+ export async function handleCatalogCommand(args: string[]): Promise<number> {
22
+ if (hasHelpFlag(args)) { printSubcommandUsage("catalog"); return 0; }
23
+ const json = args.includes("--json");
24
+ const restartCodex = args.includes("--restart-codex");
25
+ const authEnv = optionValue(args, "--auth-env");
26
+ const positionals = args.filter((arg, index) => {
27
+ if (arg === "--auth-env") return false;
28
+ if (index > 0 && args[index - 1] === "--auth-env") return false;
29
+ return !arg.startsWith("-");
30
+ });
31
+ const knownFlags = new Set(["--json", "--restart-codex", "--auth-env"]);
32
+ const unknown = args.find((arg, index) => arg.startsWith("-") && !knownFlags.has(arg) && args[index - 1] !== "--auth-env");
33
+ const validEnvName = authEnv === undefined || /^[A-Za-z_][A-Za-z0-9_]*$/.test(authEnv);
34
+ if (positionals[0] !== "pull" || positionals.length !== 2 || unknown || !validEnvName
35
+ || args.includes("--auth-env") !== (authEnv !== undefined)) {
36
+ const envelope: CatalogPullEnvelope = {
37
+ schemaVersion: 1, ok: false, status: "failed", catalogWritten: false,
38
+ cacheSynced: false, codexRestarted: false, code: "usage",
39
+ };
40
+ if (json) console.log(JSON.stringify(envelope));
41
+ else console.error("Usage: ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex]");
42
+ return 2;
43
+ }
44
+ let token: string | undefined;
45
+ if (authEnv !== undefined) {
46
+ token = process.env[authEnv];
47
+ if (token === undefined) {
48
+ const envelope: CatalogPullEnvelope = {
49
+ schemaVersion: 1, ok: false, status: "failed", catalogWritten: false,
50
+ cacheSynced: false, codexRestarted: false, code: "auth_env_missing",
51
+ };
52
+ if (json) console.log(JSON.stringify(envelope));
53
+ else console.error(`Catalog authentication environment variable ${authEnv} is not set.`);
54
+ return 1;
55
+ }
56
+ }
57
+ try {
58
+ const result = await pullRemoteCatalog(positionals[1]!, { token });
59
+ let codexRestarted = false;
60
+ let restartIncomplete = false;
61
+ if (result.catalogWritten) {
62
+ const processLog = json
63
+ ? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
64
+ : console;
65
+ const processResult = afterCatalogWriteHandleAppServers({ restart: restartCodex, log: processLog });
66
+ const restart = processResult.restart;
67
+ if (restart) {
68
+ // A partial stop is not a restart. `restartCodexAppServers` reports failures and
69
+ // survivors without throwing, so counting `stopped` alone reported success while a
70
+ // stale app-server was still serving the previous catalog from memory.
71
+ codexRestarted = restart.failed.length === 0
72
+ && restart.surviving.length === 0
73
+ && restart.stopped.length === processResult.processes.length;
74
+ restartIncomplete = !codexRestarted;
75
+ }
76
+ }
77
+ if (restartIncomplete) {
78
+ // The catalog and cache landed; only the restart did not finish. Saying the pull failed
79
+ // and wrote nothing would be a second false report, so the envelope keeps the real
80
+ // write state and `ok` carries the failure.
81
+ const envelope: CatalogPullEnvelope = {
82
+ schemaVersion: 1, ok: false, status: result.status,
83
+ catalogWritten: result.catalogWritten, cacheSynced: result.cacheSynced,
84
+ codexRestarted: false, modelCount: result.modelCount, code: "restart_incomplete",
85
+ };
86
+ if (json) console.log(JSON.stringify(envelope));
87
+ else console.error("Remote Codex catalog installed, but a Codex app-server is still running the previous catalog.");
88
+ return 1;
89
+ }
90
+ const envelope: CatalogPullEnvelope = {
91
+ schemaVersion: 1, ok: true, status: result.status,
92
+ catalogWritten: result.catalogWritten, cacheSynced: result.cacheSynced,
93
+ codexRestarted, modelCount: result.modelCount,
94
+ };
95
+ if (json) console.log(JSON.stringify(envelope));
96
+ else if (result.status === "unchanged") console.log("Remote Codex catalog is unchanged; no files or processes were touched.");
97
+ else console.log(`Remote Codex catalog installed (${result.modelCount} models) and models_cache.json synchronized.`);
98
+ return 0;
99
+ } catch (error) {
100
+ const code = error instanceof RemoteCatalogError ? error.code : "write_failed";
101
+ const envelope: CatalogPullEnvelope = {
102
+ schemaVersion: 1, ok: false, status: "failed", catalogWritten: false,
103
+ cacheSynced: false, codexRestarted: false, code,
104
+ };
105
+ if (json) console.log(JSON.stringify(envelope));
106
+ else console.error(error instanceof RemoteCatalogError ? error.message : "Remote catalog installation failed");
107
+ return code === "lock_busy" ? 3 : 1;
108
+ }
109
+ }
@@ -424,6 +424,7 @@ const commandRunners: Record<string, CommandRunner> = {
424
424
  // Explicit sync with the integration OFF still refreshes the catalog/cache
425
425
  // for side profiles that consume the proxy without injection.
426
426
  console.log(synced.message ?? "Codex integration is OFF; catalog refreshed, Codex config untouched.");
427
+ if (!synced.ok) code = 1;
427
428
  } else if (!synced.ok) {
428
429
  code = 1;
429
430
  console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
@@ -481,10 +482,18 @@ const commandRunners: Record<string, CommandRunner> = {
481
482
  const { handleConnectCommand } = await import("./connect");
482
483
  return await handleConnectCommand(deps.args.slice(1));
483
484
  },
485
+ "remote-workspace": async deps => {
486
+ const { runRemoteWorkspaceCommand } = await import("./remote-workspace");
487
+ return await runRemoteWorkspaceCommand(deps.args.slice(1));
488
+ },
484
489
  disconnect: async deps => {
485
490
  const { handleDisconnectCommand } = await import("./connect");
486
491
  return await handleDisconnectCommand(deps.args.slice(1));
487
492
  },
493
+ catalog: async deps => {
494
+ const { handleCatalogCommand } = await import("./catalog");
495
+ return await handleCatalogCommand(deps.args.slice(1));
496
+ },
488
497
  "sync-cache": async deps => {
489
498
  const cacheArgs = deps.args.slice(1);
490
499
  const restartCodex = cacheArgs.includes("--restart-codex");
package/src/cli/help.ts CHANGED
@@ -41,10 +41,12 @@ Usage:
41
41
  ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
42
42
  ocx ensure Ensure the proxy is running and Codex config/cache are current
43
43
  ocx connect <url> Connect this machine to a remote OpenCodex hub (credential via stdin)
44
+ ocx remote-workspace <sub> Pair/run an OCX-only remote execution computer
44
45
  ocx disconnect Restore local state and clear the hub connection
45
46
  ocx sync [--restart-codex] Fetch models from providers and inject into Codex config
46
47
  ocx sync-cache [--restart-codex]
47
48
  Refresh Codex's model cache from the active catalog
49
+ ocx catalog pull <https-url> Install a validated remote catalog and refresh the Codex cache
48
50
  ocx status Check proxy server status (on a hub: one block with its ports and token source)
49
51
  ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
50
52
  ocx doctor --reclaim-response-temps
package/src/cli/index.ts CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  import { requestBoundSystemRestart } from "./system-restart-client";
64
64
  import { installCrashGuards } from "../lib/crash-guard";
65
65
  import { dispatchCommand , decideStartWithLiveOwner } from "./dispatch";
66
- import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
66
+ import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
67
67
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
68
68
  import { createReadinessGate } from "../server/readiness";
69
69
  import { isApiAuthRequired } from "../server/auth-cors";
@@ -391,7 +391,7 @@ async function handleStart(options: { block?: boolean } = {}) {
391
391
  scheduleCatalogPrewarm();
392
392
  break;
393
393
  } catch (err) {
394
- if (!isAddrInUse(err) || attempt >= 2) throw err;
394
+ if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
395
395
  if (requestedPort !== undefined) {
396
396
  console.log(`⚠️ Port ${port} was taken while starting; waiting to retry the same port...`);
397
397
  const hostname = loadConfig().hostname ?? "127.0.0.1";
@@ -14,6 +14,10 @@ import { formatUsageReport } from "./usage-report";
14
14
  import { USAGE_RANGES, USAGE_SURFACES, type UsageSummary } from "../usage/summary";
15
15
  import { parseUsageTimeWindow, type UsageTimeWindow } from "../usage/time-range";
16
16
  import { redactSecretString } from "../lib/redact";
17
+ import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
18
+ import { readServiceApiTokenState } from "../lib/service-secrets";
19
+ import { fetchHubUsage } from "../client/hub-client";
20
+ import type { HubUsageReport } from "../remote/hub-usage";
17
21
 
18
22
  const USAGE = `Usage:
19
23
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
@@ -175,7 +179,30 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
175
179
  throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
176
180
  }
177
181
  rejectArgs(args.map(redactSecretString), USAGE);
178
- const result = await runtimeRequest<UsageSummary>(`/api/usage${query({ range, surface, provider, model, since: window?.since, until: window?.until })}`, {}, deps);
182
+ const suffix = query({ range, surface, provider, model, since: window?.since, until: window?.until });
183
+ const connection = readClientConnectionState();
184
+ let result: UsageSummary | HubUsageReport;
185
+ if (connection.kind === "invalid" || connection.kind === "mismatched") {
186
+ throw new Error(`Client usage unavailable: ${connection.reason}`);
187
+ }
188
+ if (connection.kind === "connected") {
189
+ const token = readServiceApiTokenState();
190
+ if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
191
+ throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
192
+ }
193
+ result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix), {
194
+ fetchImpl: deps.fetchImpl, timeoutMs: 60_000,
195
+ });
196
+ const current = readClientConnectionState();
197
+ const currentToken = readServiceApiTokenState();
198
+ if (current.kind !== "connected" || !sameClientConnectionOwner(current.value, connection.value)
199
+ || current.value.tokenFingerprint !== token.fingerprint
200
+ || currentToken.kind !== "present" || currentToken.fingerprint !== token.fingerprint) {
201
+ throw new Error("Client connection changed while reading usage; retry for the current connection");
202
+ }
203
+ } else {
204
+ result = await runtimeRequest<UsageSummary>(`/api/usage${suffix}`, {}, deps);
205
+ }
179
206
  // Older daemons ignore custom bounds and return successful preset reports.
180
207
  if (window && (result?.customWindow !== true || result.since !== window.since || result.until !== window.until)) {
181
208
  throw new Error("The server did not confirm the requested custom usage window. Upgrade and restart the proxy, then retry.");
@@ -41,6 +41,9 @@ import type {
41
41
  } from "../clients/config-export";
42
42
  import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
43
43
  import { commandInvocation } from "../lib/win-exec";
44
+ import { configuredAdminToken } from "../lib/admin-secrets";
45
+ import { localManagementOrigin } from "../lib/local-destinations";
46
+ import { directLocalHttpFetch } from "../server/direct-local-http";
44
47
  import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
45
48
  import { providerCodexAccountMode } from "../providers/registry";
46
49
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
@@ -306,17 +309,38 @@ function opencodeBlocks(
306
309
  /** Default deadline for authenticated GET /api/models during `ocx opencode` launch. */
307
310
  export const OPENCODE_PROXY_MODELS_TIMEOUT_MS = 8_000;
308
311
 
312
+ function opencodeManagementOrigin(live: LiveProxy, override?: string): string {
313
+ if (!override && (!Number.isInteger(live.port) || live.port < 1 || live.port > 65535)) {
314
+ throw new Error("The local management port is invalid.");
315
+ }
316
+ let url: URL;
317
+ try { url = new URL(override ?? `http://${probeHostname(live.hostname)}:${live.port}`); }
318
+ catch { throw new Error("The local management address is invalid."); }
319
+ if (url.protocol !== "http:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
320
+ throw new Error("The catalog requires a local HTTP management origin without credentials or a path.");
321
+ }
322
+ const host = url.hostname.toLowerCase();
323
+ if (["localhost", "localhost.", "127.0.0.1", "0.0.0.0", "[::]"].includes(host)) url.hostname = "127.0.0.1";
324
+ else if (host !== "[::1]") {
325
+ throw new Error("The catalog requires a loopback management listener. On a hub, enable hub.managementIngress.");
326
+ }
327
+ const port = Number(url.port || 80);
328
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("The local management port is invalid.");
329
+ return url.origin;
330
+ }
331
+
309
332
  /** Fetch the live model catalog from a running proxy's management API. */
310
333
  export async function fetchOpencodeProxyModels(
311
334
  live: LiveProxy,
312
- apiKey: string,
313
- deps: { fetchImpl?: typeof fetch; timeoutMs?: number } = {},
335
+ managementToken: string,
336
+ deps: { fetchImpl?: typeof fetch; timeoutMs?: number; managementOrigin?: string } = {},
314
337
  ): Promise<OpencodeProxyModelRow[]> {
315
- const baseUrl = `http://${probeHostname(live.hostname)}:${live.port}`;
316
- const fetchImpl = deps.fetchImpl ?? fetch;
338
+ const baseUrl = opencodeManagementOrigin(live, deps.managementOrigin);
339
+ const fetchImpl = deps.fetchImpl ?? directLocalHttpFetch;
317
340
  const headers = new Headers({ Accept: "application/json" });
318
- const token = apiKey.trim();
319
- if (token) headers.set("X-OpenCodex-API-Key", token);
341
+ const token = managementToken.trim();
342
+ if (!token) throw new Error("No local admin token is available for the model catalog.");
343
+ headers.set("X-OpenCodex-API-Key", token);
320
344
  const controller = new AbortController();
321
345
  const timeout = setTimeout(() => controller.abort(), deps.timeoutMs ?? OPENCODE_PROXY_MODELS_TIMEOUT_MS);
322
346
  const abortIfTimedOut = (): Promise<never> => new Promise((_, reject) => {
@@ -337,10 +361,16 @@ export async function fetchOpencodeProxyModels(
337
361
  response = await Promise.race([
338
362
  fetchImpl(`${baseUrl}/api/models`, {
339
363
  headers,
364
+ redirect: "error",
365
+ cache: "no-store",
340
366
  signal: controller.signal,
341
367
  }),
342
368
  abortIfTimedOut(),
343
369
  ]);
370
+ if (response.status >= 300 && response.status < 400) {
371
+ void response.body?.cancel().catch(() => {});
372
+ throw new Error("Management catalog redirects are refused.");
373
+ }
344
374
  text = await Promise.race([response.text(), abortIfTimedOut()]);
345
375
  } catch (error) {
346
376
  const timedOut = error instanceof Error && error.name === "AbortError";
@@ -584,7 +614,7 @@ export function buildOpencodeEnv(
584
614
  const runtimeConfig = mergeOpencodeRuntimeConfig(base[OPENCODE_CONFIG_CONTENT_ENV], blocks);
585
615
  if (isOpencodeRuntimeConfigError(runtimeConfig)) return runtimeConfig;
586
616
  return {
587
- ...base,
617
+ ...Object.fromEntries(Object.entries(base).filter(([name]) => name.toUpperCase() !== "OPENCODEX_ADMIN_AUTH_TOKEN")),
588
618
  [OPENCODE_CONFIG_CONTENT_ENV]: serializeOpencodeRuntimeConfig(runtimeConfig),
589
619
  [OPENCODE_API_KEY_ENV]: apiKey,
590
620
  };
@@ -652,7 +682,11 @@ export async function cmdOpencode(args: string[]): Promise<number> {
652
682
  const apiKey = opencodeApiKey(startupConfig);
653
683
  let proxyModels: OpencodeProxyModelRow[];
654
684
  try {
655
- proxyModels = await fetchOpencodeProxyModels(live, apiKey);
685
+ const managementToken = configuredAdminToken();
686
+ if (!managementToken) throw new Error("No local admin token is available; check the running proxy's home.");
687
+ proxyModels = await fetchOpencodeProxyModels(live, managementToken, {
688
+ managementOrigin: localManagementOrigin({ ...startupConfig, hostname: live.hostname }, live.port),
689
+ });
656
690
  } catch (error) {
657
691
  const reason = error instanceof Error ? error.message : String(error);
658
692
  console.error(`❌ Could not fetch the model catalog from the proxy: ${reason}`);
@@ -1,3 +1,4 @@
1
+ import { modelCapabilitiesConfigError } from "../config/provider-validation";
1
2
  import {
2
3
  CliUsageError,
3
4
  csv,
@@ -39,7 +40,7 @@ const USAGE = `Usage:
39
40
  [--auth-mode <key|forward|oauth|local|->] [--note <text|->]
40
41
  [--api-key-transport <x-api-key|bearer|->]
41
42
  [--headers <json>] [--enabled <on|off>] [--live-models <on|off>]
42
- [--retain-models <id,id|->]
43
+ [--retain-models <id,id|->] [--model <id> --text-only]
43
44
  [--xai-chat <on|off>]
44
45
  [--allow-private-network <on|off>] [--json]
45
46
  ocx provider test <name> [--json]
@@ -72,7 +73,16 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
72
73
  const liveModels = takeBooleanOption(args, "--live-models");
73
74
  const allowPrivateNetwork = takeBooleanOption(args, "--allow-private-network");
74
75
  const xaiChat = takeBooleanOption(args, "--xai-chat");
76
+ const textOnly = takeFlag(args, "--text-only");
77
+ const capabilityModel = takeOption(args, "--model");
75
78
  rejectArgs(args, USAGE);
79
+ if (textOnly || capabilityModel !== undefined) {
80
+ if (!textOnly || capabilityModel === undefined) throw new CliUsageError("--text-only and --model must be supplied together", USAGE);
81
+ const declaration = { [capabilityModel]: { inputModalities: ["text"] } };
82
+ const error = modelCapabilitiesConfigError(declaration);
83
+ if (error) throw new CliUsageError(error, USAGE);
84
+ patch.modelCapabilities = declaration;
85
+ }
76
86
  if (xaiChat !== undefined) {
77
87
  if (name !== "xai") throw new CliUsageError("--xai-chat is valid only for provider xai", USAGE);
78
88
  patch.xaiResponsesOptIn = !xaiChat;
@@ -9,7 +9,7 @@
9
9
  * set-default <name> Change the default provider
10
10
  */
11
11
  import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
12
- import { apiKeyTransportConfigError } from "../config/provider-validation";
12
+ import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
13
13
  import { hasHelpFlag } from "./help";
14
14
  import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
15
15
  import { providerConfigSeed } from "../providers/derive";
@@ -139,7 +139,7 @@ function handleList(args: string[]): void {
139
139
  // provider add
140
140
  // ---------------------------------------------------------------------------
141
141
 
142
- const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
142
+ const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
143
143
 
144
144
  async function handleAdd(args: string[]): Promise<void> {
145
145
  const name = args[0];
@@ -164,7 +164,13 @@ async function handleAdd(args: string[]): Promise<void> {
164
164
  const adapter = consumeFlagValue(restArgs, "--adapter");
165
165
  const baseUrl = consumeFlagValue(restArgs, "--base-url");
166
166
  const defaultModel = consumeFlagValue(restArgs, "--default-model");
167
+ const textOnly = consumeFlag(restArgs, "--text-only");
168
+ const capabilityModel = consumeFlagValue(restArgs, "--model");
167
169
  rejectUnknownArgs(restArgs, ADD_USAGE);
170
+ if (capabilityModel !== undefined && !textOnly) {
171
+ console.error("Error: --model requires --text-only for provider add.");
172
+ process.exit(1);
173
+ }
168
174
 
169
175
  const config = loadConfig();
170
176
 
@@ -224,6 +230,20 @@ async function handleAdd(args: string[]): Promise<void> {
224
230
  }
225
231
 
226
232
  const existingProvider = config.providers[name];
233
+ if (existingProvider?.modelCapabilities !== undefined && provConfig.modelCapabilities === undefined) {
234
+ provConfig.modelCapabilities = structuredClone(existingProvider.modelCapabilities);
235
+ }
236
+ if (textOnly) {
237
+ const modelId = capabilityModel ?? defaultModel ?? provConfig.defaultModel;
238
+ if (!modelId) {
239
+ console.error("Error: --text-only requires --model or a default model.");
240
+ process.exit(1);
241
+ }
242
+ const declaration = { [modelId]: { inputModalities: ["text"] } };
243
+ const error = modelCapabilitiesConfigError(declaration);
244
+ if (error) { console.error(`Error: ${error}.`); process.exit(1); }
245
+ provConfig.modelCapabilities = mergeModelCapabilities(provConfig.modelCapabilities, declaration);
246
+ }
227
247
  const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
228
248
  initializeProviderModelSelection(name, provConfig, existingProvider, config);
229
249
  config.providers[name] = provConfig;
@@ -107,6 +107,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
107
107
  "Credentials are accepted only through stdin; argv and environment credential forms are not supported.",
108
108
  ],
109
109
  },
110
+ {
111
+ name: "remote-workspace",
112
+ usage: "ocx remote-workspace <pair|agent|status>",
113
+ summary: "Pair this OCX-only computer with a hub and run its remote workspace executor.",
114
+ details: [
115
+ "Pair: ocx remote-workspace pair <hub-url> --pairing-code-stdin --root <absolute-path> [--toolchain-root <absolute-directory>] [--executor-helper <absolute-file>] [--name <device-name>]",
116
+ "Agent: ocx remote-workspace agent",
117
+ "Status: ocx remote-workspace status [--json]",
118
+ "Codex, Claude Code, Pi, provider logins, and API keys remain on the hub; only workspace tools execute here.",
119
+ ],
120
+ },
110
121
  {
111
122
  name: "disconnect",
112
123
  usage: "ocx disconnect [--keep-catalog] [--json]",
@@ -132,6 +143,16 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
132
143
  "--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
133
144
  ],
134
145
  },
146
+ {
147
+ name: "catalog",
148
+ usage: "ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex]",
149
+ summary: "Install a validated remote /v1/catalog snapshot into Codex.",
150
+ details: [
151
+ "Authentication is read only from the named environment variable and sent as a Bearer token.",
152
+ "HTTPS is required except for loopback HTTP; redirects are refused.",
153
+ "The catalog and models_cache.json are coordinated under the Codex catalog write lock.",
154
+ ],
155
+ },
135
156
  { name: "status", usage: "ocx status", summary: "Check proxy server status." },
136
157
  {
137
158
  name: "doctor",