@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `ocx storage` — the archived-session cleanup, trash, and cleanup-policy surface (wp7).
3
+ *
4
+ * Every route here existed with no CLI caller, so reclaiming disk space was dashboard-only.
5
+ * Three of them delete or move operator data, and the rules for those are deliberate:
6
+ *
7
+ * 1. **Default to preview.** `ocx storage cleanup --percent N` runs the preview route and prints
8
+ * what WOULD be freed, then exits 0 having mutated nothing.
9
+ * 2. **`--yes` is required to mutate.** There is no interactive prompt: an agent cannot answer
10
+ * one, and a prompt an agent can answer is not a safety boundary.
11
+ * 3. **`--json` on the preview emits the candidate list**, so an agent can decide from data
12
+ * rather than from a sentence.
13
+ *
14
+ * This is the opposite of the GitHub star POST, which no flag can authorize: cleanup spends the
15
+ * operator's DATA, which they can delegate, while starring spends their IDENTITY, which they
16
+ * cannot delegate to an agent.
17
+ */
18
+ import {
19
+ CliUsageError,
20
+ printData,
21
+ rejectArgs,
22
+ runCliAction,
23
+ runtimeRequest,
24
+ summaryLines,
25
+ takeFlag,
26
+ takeIntegerOption,
27
+ takeOption,
28
+ type RuntimeApiDeps,
29
+ } from "./runtime-api";
30
+
31
+ const USAGE = `Usage:
32
+ ocx storage report [--json]
33
+ ocx storage cleanup --percent <0-100> [--mode <quarantine|permanent>] [--yes] [--json]
34
+ ocx storage trash [list] [--json]
35
+ ocx storage trash restore <entry-id> [--yes] [--json]
36
+ ocx storage policy [show] [--json]
37
+ ocx storage policy set [--enabled <true|false>] [--percent <0-100>]
38
+ [--mode <quarantine|permanent>] [--schedule <startup|daily|weekly|manual>] [--json]
39
+ ocx storage policy run [--yes] [--json]
40
+
41
+ Cleanup and restore MUTATE operator data and require --yes.
42
+ Without --yes, cleanup prints the preview and changes nothing.`;
43
+
44
+ /** The digest binds a run to the preview it was authorized against. */
45
+ interface CleanupPreview {
46
+ percent?: number;
47
+ count?: number;
48
+ bytes?: number;
49
+ digest?: string;
50
+ candidates?: { relPath?: string; bytes?: number }[];
51
+ }
52
+
53
+ function mib(bytes: number | undefined): string {
54
+ if (typeof bytes !== "number" || !Number.isFinite(bytes)) return "unknown size";
55
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
56
+ }
57
+
58
+ function previewLines(preview: CleanupPreview): string[] {
59
+ const lines = [
60
+ `Would remove ${preview.count ?? 0} archived session file(s), freeing ${mib(preview.bytes)}.`,
61
+ ];
62
+ for (const candidate of (preview.candidates ?? []).slice(0, 10)) {
63
+ lines.push(` ${candidate.relPath ?? "(unnamed)"} ${mib(candidate.bytes)}`);
64
+ }
65
+ const shown = Math.min((preview.candidates ?? []).length, 10);
66
+ if ((preview.count ?? 0) > shown) lines.push(` … and ${(preview.count ?? 0) - shown} more`);
67
+ lines.push("Nothing was deleted. Re-run with --yes to apply.");
68
+ return lines;
69
+ }
70
+
71
+ async function cleanup(argv: string[], deps: RuntimeApiDeps): Promise<void> {
72
+ const args = [...argv];
73
+ const wantsJson = takeFlag(args, "--json");
74
+ const confirmed = takeFlag(args, "--yes");
75
+ const percent = takeIntegerOption(args, "--percent", { min: 0 });
76
+ const mode = takeOption(args, "--mode") ?? "quarantine";
77
+ rejectArgs(args, USAGE);
78
+
79
+ if (percent === undefined) throw new CliUsageError("--percent is required", USAGE);
80
+ if (percent > 100) throw new CliUsageError("--percent must be between 0 and 100", USAGE);
81
+ if (mode !== "quarantine" && mode !== "permanent") {
82
+ throw new CliUsageError("--mode must be quarantine or permanent", USAGE);
83
+ }
84
+
85
+ // The preview runs in BOTH paths, and not only to be friendly: the mutating route requires the
86
+ // digest this call returns and rejects a stale one with 409 `stale_preview`. So the confirmed
87
+ // path cannot skip it, which conveniently means --yes and no --yes agree on what they mean.
88
+ const preview = await runtimeRequest<CleanupPreview>("/api/storage/cleanup/preview", {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify({ percent }),
92
+ }, deps);
93
+
94
+ if (!confirmed) {
95
+ printData(preview, wantsJson, previewLines(preview));
96
+ return;
97
+ }
98
+
99
+ if (!preview.digest) {
100
+ // Refuse rather than send an empty digest: the server would reject it, but a clear local
101
+ // message beats a 400 that looks like a bug in the verb.
102
+ throw new CliUsageError("the preview returned no digest, so the cleanup cannot be authorized", USAGE);
103
+ }
104
+
105
+ const result = await runtimeRequest("/api/storage/cleanup", {
106
+ method: "POST",
107
+ headers: { "content-type": "application/json" },
108
+ body: JSON.stringify({ percent, mode, digest: preview.digest }),
109
+ }, deps);
110
+ printData(result, wantsJson, summaryLines(result));
111
+ }
112
+
113
+ async function trash(argv: string[], deps: RuntimeApiDeps): Promise<void> {
114
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
115
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
116
+
117
+ if (action === "list") {
118
+ const args = [...rest];
119
+ const wantsJson = takeFlag(args, "--json");
120
+ rejectArgs(args, USAGE);
121
+ const result = await runtimeRequest("/api/storage/trash", {}, deps);
122
+ printData(result, wantsJson, summaryLines(result));
123
+ return;
124
+ }
125
+
126
+ if (action !== "restore") throw new CliUsageError(`unknown trash action ${action}`, USAGE);
127
+
128
+ const args = [...rest];
129
+ const wantsJson = takeFlag(args, "--json");
130
+ const confirmed = takeFlag(args, "--yes");
131
+ const id = args.shift();
132
+ rejectArgs(args, USAGE);
133
+ if (!id) throw new CliUsageError("a trash entry id is required", USAGE);
134
+
135
+ // Restore moves files back and reconciles database rows, and can collide with an existing
136
+ // destination, so it is gated like cleanup rather than treated as a read.
137
+ if (!confirmed) {
138
+ throw new CliUsageError(`restoring ${id} modifies stored sessions; pass --yes to confirm`, USAGE);
139
+ }
140
+
141
+ const result = await runtimeRequest("/api/storage/trash/restore", {
142
+ method: "POST",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify({ id }),
145
+ }, deps);
146
+ printData(result, wantsJson, summaryLines(result));
147
+ }
148
+
149
+ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "show";
151
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
152
+
153
+ if (action === "show") {
154
+ const args = [...rest];
155
+ const wantsJson = takeFlag(args, "--json");
156
+ rejectArgs(args, USAGE);
157
+ const result = await runtimeRequest("/api/storage/cleanup-policy", {}, deps);
158
+ printData(result, wantsJson, summaryLines(result));
159
+ return;
160
+ }
161
+
162
+ if (action === "set") {
163
+ const args = [...rest];
164
+ const wantsJson = takeFlag(args, "--json");
165
+ const enabled = takeOption(args, "--enabled");
166
+ const percent = takeIntegerOption(args, "--percent", { min: 0 });
167
+ const mode = takeOption(args, "--mode");
168
+ const schedule = takeOption(args, "--schedule");
169
+ rejectArgs(args, USAGE);
170
+
171
+ if (enabled !== undefined && enabled !== "true" && enabled !== "false") {
172
+ throw new CliUsageError("--enabled must be true or false", USAGE);
173
+ }
174
+ const body: Record<string, unknown> = {};
175
+ if (enabled !== undefined) body.enabled = enabled === "true";
176
+ if (percent !== undefined) body.percent = percent;
177
+ if (mode !== undefined) body.mode = mode;
178
+ if (schedule !== undefined) body.schedule = schedule;
179
+ if (Object.keys(body).length === 0) {
180
+ throw new CliUsageError("policy set needs at least one of --enabled, --percent, --mode, --schedule", USAGE);
181
+ }
182
+ // Values are NOT re-validated here beyond --enabled's shape. The server owns the mode and
183
+ // schedule vocabularies and returns a named 400; duplicating them is a second thing to
184
+ // keep in sync. `enabled` is checked because "--enabled maybe" would otherwise be sent as
185
+ // `false`, which is a wrong write rather than a rejected one.
186
+ const result = await runtimeRequest("/api/storage/cleanup-policy", {
187
+ method: "PUT",
188
+ headers: { "content-type": "application/json" },
189
+ body: JSON.stringify(body),
190
+ }, deps);
191
+ printData(result, wantsJson, summaryLines(result));
192
+ return;
193
+ }
194
+
195
+ if (action !== "run") throw new CliUsageError(`unknown policy action ${action}`, USAGE);
196
+
197
+ const args = [...rest];
198
+ const wantsJson = takeFlag(args, "--json");
199
+ const confirmed = takeFlag(args, "--yes");
200
+ rejectArgs(args, USAGE);
201
+ // `force: true` server-side: this run ignores the schedule and deletes now.
202
+ if (!confirmed) {
203
+ throw new CliUsageError("policy run deletes archived sessions now; pass --yes to confirm", USAGE);
204
+ }
205
+ const result = await runtimeRequest("/api/storage/cleanup-policy/run", { method: "POST" }, deps);
206
+ printData(result, wantsJson, summaryLines(result));
207
+ }
208
+
209
+ export async function handleStorageCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
210
+ const hasSub = argv[0] !== undefined && !argv[0].startsWith("-");
211
+ const sub = hasSub ? argv[0]! : "report";
212
+ const rest = hasSub ? argv.slice(1) : argv;
213
+ if (sub === "codex-logs") {
214
+ // Doctor and the Log Guard guides still document `ocx storage codex-logs …`.
215
+ // This module owns cleanup/trash/policy; log-guard stays on the observe handler.
216
+ const { handleObserveCommand } = await import("./observe");
217
+ return handleObserveCommand(["storage", "codex-logs", ...rest], deps);
218
+ }
219
+ return runCliAction(async () => {
220
+ if (sub === "report") {
221
+ const args = [...rest];
222
+ const wantsJson = takeFlag(args, "--json");
223
+ rejectArgs(args, USAGE);
224
+ const result = await runtimeRequest("/api/storage", {}, deps);
225
+ printData(result, wantsJson, summaryLines(result));
226
+ }
227
+ else if (sub === "cleanup") await cleanup(rest, deps);
228
+ else if (sub === "trash") await trash(rest, deps);
229
+ else if (sub === "policy") await policy(rest, deps);
230
+ else throw new CliUsageError(`unknown storage command ${sub}`, USAGE);
231
+ });
232
+ }
233
+
234
+ export const STORAGE_USAGE = USAGE;
@@ -17,6 +17,8 @@ const USAGE = `Usage:
17
17
  ocx system startup <health|install-service|install-shim> [--json]
18
18
  ocx system diagnostics [--json]
19
19
  ocx system sync [--json]
20
+ ocx system codex-app-server [--json]
21
+ ocx system codex-restart --yes [--json]
20
22
  ocx system update check [--channel <latest|preview>] [--json]
21
23
  ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
22
24
  ocx system update status <job-id> [--json]`;
@@ -104,6 +106,20 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
104
106
  } else if (sub === "sync") {
105
107
  const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
106
108
  printData(await runtimeRequest("/api/sync", { method: "POST" }, deps), wantsJson);
109
+ } else if (sub === "codex-app-server") {
110
+ // The GUI reads this state directly (gui/src/codex-app-server-state.ts). Without a verb
111
+ // an agent could not see whether the Codex app-server was reachable at all.
112
+ const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
113
+ printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
114
+ } else if (sub === "codex-restart") {
115
+ // --yes required: this restarts the user's running Codex app-server, so it is exactly the
116
+ // class of action that must not happen because an agent guessed a subcommand.
117
+ const args = [...rest];
118
+ const wantsJson = takeFlag(args, "--json");
119
+ const yes = takeFlag(args, "--yes");
120
+ if (!yes) throw new CliUsageError("system codex-restart requires --yes", USAGE);
121
+ rejectArgs(args, USAGE);
122
+ printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex app-server restart requested."]);
107
123
  } else if (sub === "update") await update(rest, deps);
108
124
  else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
109
125
  });
@@ -1,4 +1,4 @@
1
- import { readRuntimePort } from "../config";
1
+ import { readRuntimePort } from "../config/process-state";
2
2
  import {
3
3
  LOCAL_ATTESTATION_CHALLENGE_HEADER,
4
4
  LOCAL_ATTESTATION_PROOF_HEADER,
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Human rendering for `ocx usage`.
3
+ *
4
+ * Kept out of `observe.ts` and away from the shared `summaryLines()` helper on
5
+ * purpose. `summaryLines()` is a generic depth-1 flattener shared with
6
+ * storage/memory/debug/claude-inbound/injection; it renders any array as
7
+ * "N item(s)", which is why every per-model and per-provider cost the server
8
+ * computes used to vanish before reaching the terminal. Deepening it would
9
+ * change five unrelated commands.
10
+ *
11
+ * Formatting follows the existing CLI house style: dynamic `padEnd` columns
12
+ * (as in `formatAccountTable`), plain text, no ANSI colour.
13
+ */
14
+
15
+ interface CostRow {
16
+ provider: string;
17
+ model?: string;
18
+ requests: number;
19
+ totalTokens: number;
20
+ estimatedCostUsd?: number;
21
+ }
22
+
23
+ interface UsageReportInput {
24
+ range?: string;
25
+ surface?: string;
26
+ since?: number | null;
27
+ summary?: {
28
+ requests?: number;
29
+ totalTokens?: number;
30
+ inputTokens?: number;
31
+ outputTokens?: number;
32
+ cachedInputTokens?: number;
33
+ estimatedCostUsd?: number;
34
+ unpricedRequests?: number;
35
+ unmeteredRequests?: number;
36
+ };
37
+ models?: CostRow[];
38
+ providers?: CostRow[];
39
+ days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
40
+ filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
41
+ /**
42
+ * Per-account totals the API already returns and the CLI discarded (#2700).
43
+ *
44
+ * Withheld by the server -- sent as `[]` -- whenever a provider or model filter is active,
45
+ * because account rows are not provider-partitioned in a way the projection could honestly
46
+ * re-derive (summary.ts, reasoned around :865-872). An empty array therefore means two very
47
+ * different things, and the renderer must say which.
48
+ */
49
+ accounts?: {
50
+ accountLogLabel: string;
51
+ ambiguous?: boolean;
52
+ requests: number;
53
+ totalTokens: number;
54
+ estimatedCostUsd?: number;
55
+ }[];
56
+ }
57
+
58
+ const MAX_MODEL_ROWS = 10;
59
+
60
+ function terminalText(value: string): string {
61
+ return value.replace(/[\x00-\x1f\x7f-\x9f]/g, character => {
62
+ const code = character.charCodeAt(0);
63
+ return code <= 0x7f
64
+ ? `\\x${code.toString(16).padStart(2, "0")}`
65
+ : `\\u${code.toString(16).padStart(4, "0")}`;
66
+ });
67
+ }
68
+
69
+ function count(value: number | undefined): string {
70
+ return (value ?? 0).toLocaleString("en-US");
71
+ }
72
+
73
+ /**
74
+ * Matches the dashboard's `~$` with four fraction digits. Estimates below a
75
+ * hundredth of a cent still read as a number rather than collapsing to $0.00,
76
+ * which matters when a single request is being inspected.
77
+ */
78
+ function usd(value: number | undefined): string {
79
+ if (typeof value !== "number" || !Number.isFinite(value)) return "—";
80
+ return `~$${value.toFixed(4)}`;
81
+ }
82
+
83
+ function table(header: string[], rows: string[][]): string[] {
84
+ if (rows.length === 0) return [];
85
+ header = header.map(terminalText);
86
+ rows = rows.map(row => row.map(terminalText));
87
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
88
+ const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
89
+ return [line(header), ...rows.map(line)];
90
+ }
91
+
92
+ function describeScope(data: UsageReportInput): string {
93
+ const parts = [`Usage — ${data.range ?? "?"}`];
94
+ if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
95
+ if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
96
+ if (data.filter?.model) parts.push(`model=${data.filter.model}`);
97
+ return terminalText(parts.join(", "));
98
+ }
99
+
100
+ export function formatUsageReport(data: UsageReportInput): string[] {
101
+ const summary = data.summary ?? {};
102
+ const lines: string[] = [describeScope(data), ""];
103
+
104
+ if (data.filter && !data.filter.matched) {
105
+ const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
106
+ .filter(Boolean).join(" and ");
107
+ lines.push(`No usage recorded for ${terminalText(what)} in this range.`);
108
+ lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
109
+ return lines;
110
+ }
111
+
112
+ const tokenSplit = [
113
+ summary.inputTokens !== undefined ? `in ${count(summary.inputTokens)}` : null,
114
+ summary.outputTokens !== undefined ? `out ${count(summary.outputTokens)}` : null,
115
+ summary.cachedInputTokens ? `cached ${count(summary.cachedInputTokens)}` : null,
116
+ ].filter(Boolean).join(" / ");
117
+
118
+ lines.push(`Requests ${count(summary.requests)}`);
119
+ lines.push(`Tokens ${count(summary.totalTokens)}${tokenSplit ? ` (${tokenSplit})` : ""}`);
120
+ lines.push(`Est. cost ${usd(summary.estimatedCostUsd)} API list-price equivalent (this range)`);
121
+
122
+ const unpriced = summary.unpricedRequests ?? 0;
123
+ const unmetered = summary.unmeteredRequests ?? 0;
124
+ if (unpriced > 0 || unmetered > 0) {
125
+ // Spelled out because a $0 total is ambiguous otherwise: it can mean "no
126
+ // spend" or "no price row matched", and those are very different answers.
127
+ lines.push(` ${count(unpriced)} unpriced, ${count(unmetered)} unmetered excluded from ~$`);
128
+ }
129
+
130
+ const providers = (data.providers ?? []).filter(row => row.requests > 0);
131
+ if (providers.length > 0) {
132
+ lines.push("");
133
+ lines.push(...table(
134
+ ["PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
135
+ providers.map(row => [row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
136
+ ));
137
+ }
138
+
139
+ // Per-account spend (#2700). The two empty cases are distinguished deliberately: printing an
140
+ // empty table under a filter would repeat the silently-wrong-output defect this unit removes,
141
+ // because "what did this provider cost me per account" is the most natural way to ask and the
142
+ // server cannot answer it honestly.
143
+ const accountFilterActive = Boolean(data.filter?.provider || data.filter?.model);
144
+ const accounts = (data.accounts ?? []).filter(row => row.requests > 0);
145
+ if (accountFilterActive) {
146
+ lines.push("");
147
+ lines.push("ACCOUNT: not reported under a provider or model filter; run without filters for per-account totals.");
148
+ } else if (accounts.length > 0) {
149
+ lines.push("");
150
+ lines.push(...table(
151
+ ["ACCOUNT", "REQUESTS", "TOKENS", "EST. COST"],
152
+ accounts.map(row => [
153
+ // An ambiguous row aggregates several accounts, so reading it as one account draws the
154
+ // wrong conclusion. Mark it rather than presenting it as a single identity.
155
+ row.ambiguous ? `${terminalText(row.accountLogLabel)} (ambiguous)` : terminalText(row.accountLogLabel),
156
+ count(row.requests),
157
+ count(row.totalTokens),
158
+ usd(row.estimatedCostUsd),
159
+ ]),
160
+ ));
161
+ }
162
+
163
+ const models = (data.models ?? []).filter(row => row.requests > 0);
164
+ if (models.length > 0) {
165
+ lines.push("");
166
+ const shown = models.slice(0, MAX_MODEL_ROWS);
167
+ lines.push(...table(
168
+ ["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
169
+ shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
170
+ ));
171
+ if (models.length > shown.length) {
172
+ lines.push(`... ${models.length - shown.length} more (use --json)`);
173
+ }
174
+ }
175
+
176
+ if (data.filter?.comboOverlap) {
177
+ lines.push("");
178
+ lines.push("Some requests ran as combos, so per-model request counts can overlap. Cost does not.");
179
+ }
180
+
181
+ lines.push("");
182
+ lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
183
+ return lines;
184
+ }
package/src/cli/v2.ts CHANGED
@@ -15,7 +15,7 @@ import { dirname } from "node:path";
15
15
  import { activeCodexConfigPath, getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
16
16
 
17
17
  import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
18
- import { loadConfig, saveConfig } from "../config";
18
+ import { deleteConfigTopLevelKey, loadConfig, saveConfig } from "../config";
19
19
  import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
20
20
 
21
21
  export interface V2CliDeps {
@@ -88,18 +88,24 @@ function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
88
88
 
89
89
  export function v2StatusLine(enabled: boolean): string {
90
90
  return enabled
91
- ? "multi_agent_v2: ON — v2 multi-agent surface active"
92
- : "multi_agent_v2: OFF — v1 multi-agent surface (default install)";
91
+ ? "multi_agent_v2: ON — global V2 override active"
92
+ : "multi_agent_v2: OFF — model catalog pins and defaults decide the surface";
93
93
  }
94
94
 
95
- export function multiAgentModeLine(mode: string): string {
95
+ export function multiAgentModeLine(mode: string, keepNativeChatGptOnV1 = false): string {
96
96
  switch (mode) {
97
97
  case "v1": return "multi_agent_mode: v1 — ALL models forced to v1 surface (upstream pins overridden)";
98
- case "v2": return "multi_agent_mode: v2 — ALL models forced to v2 surface (upstream pins overridden)";
98
+ case "v2": return keepNativeChatGptOnV1
99
+ ? "multi_agent_mode: v2 hybrid — ChatGPT-native models use v1; routed models use v2"
100
+ : "multi_agent_mode: v2 — ALL models forced to v2 surface (upstream pins overridden)";
99
101
  default: return "multi_agent_mode: default — upstream model pins respected (sol/terra=v2, luna=v1, rest=codex flag)";
100
102
  }
101
103
  }
102
104
 
105
+ function requiresGlobalV2Disabled(multiAgentMode: string | undefined, keepNativeChatGptOnV1: boolean): boolean {
106
+ return multiAgentMode === "v2" && keepNativeChatGptOnV1;
107
+ }
108
+
103
109
  export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
104
110
  const log = deps.log ?? console;
105
111
  const isEnabled = deps.isEnabled ?? isMultiAgentV2Enabled;
@@ -109,9 +115,13 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
109
115
  if (verb === "status") {
110
116
  log.log(v2StatusLine(isEnabled()));
111
117
  const cfg = loadConfig();
112
- log.log(multiAgentModeLine(cfg.multiAgentMode ?? "default"));
118
+ const mode = cfg.multiAgentMode ?? "default";
119
+ const keepNativeV1 = cfg.keepNativeChatGptOnV1 === true;
120
+ log.log(multiAgentModeLine(mode, keepNativeV1));
113
121
  log.log(cfg.keepNativeChatGptOnV1 === true
114
- ? "keep_native_chatgpt_on_v1: ON ChatGPT-native rows stay v1 when mode is v2"
122
+ ? requiresGlobalV2Disabled(mode, keepNativeV1) && isEnabled()
123
+ ? "keep_native_chatgpt_on_v1: CONFLICT — global multi_agent_v2 overrides the native v1 catalog pin; run 'ocx v2 keep-native-v1 on' to reconcile"
124
+ : "keep_native_chatgpt_on_v1: ON — global V2 override is off; ChatGPT-native rows use v1 and routed rows use v2 when mode is v2"
115
125
  : "keep_native_chatgpt_on_v1: OFF");
116
126
  const threads = getLogicalMaxThreads();
117
127
  log.log(`max_threads: ${threads ?? "(unset — codex default)"}`);
@@ -186,14 +196,14 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
186
196
  }
187
197
  const cfg = loadConfig();
188
198
  if (modeArg !== "default") {
189
- const target = modeArg === "v2";
199
+ const target = modeArg === "v2" && cfg.keepNativeChatGptOnV1 !== true;
190
200
  const transition = transitionMultiAgentV2(target, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
191
201
  if (!transition.ok) {
192
202
  log.error(`multi-agent mode transition failed: ${transition.error}`);
193
203
  return 1;
194
204
  }
195
205
  }
196
- if (modeArg === "default") delete cfg.multiAgentMode;
206
+ if (modeArg === "default") deleteConfigTopLevelKey(cfg, "multiAgentMode");
197
207
  else cfg.multiAgentMode = modeArg as "v1" | "v2";
198
208
  saveConfig(cfg);
199
209
  try {
@@ -216,8 +226,15 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
216
226
  const cfg = loadConfig();
217
227
  const next = flag === "on";
218
228
  const already = cfg.keepNativeChatGptOnV1 === true === next;
229
+ if (next && requiresGlobalV2Disabled(cfg.multiAgentMode, true)) {
230
+ const transition = transitionMultiAgentV2(false, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
231
+ if (!transition.ok) {
232
+ log.error(`keep-native-v1 transition failed: ${transition.error}`);
233
+ return 1;
234
+ }
235
+ }
219
236
  if (next) cfg.keepNativeChatGptOnV1 = true;
220
- else delete cfg.keepNativeChatGptOnV1;
237
+ else deleteConfigTopLevelKey(cfg, "keepNativeChatGptOnV1");
221
238
  saveConfig(cfg);
222
239
  try {
223
240
  const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
@@ -243,6 +260,13 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
243
260
  }
244
261
 
245
262
  const want = verb === "on";
263
+ if (want) {
264
+ const cfg = loadConfig();
265
+ if (requiresGlobalV2Disabled(cfg.multiAgentMode, cfg.keepNativeChatGptOnV1 === true)) {
266
+ log.error("v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first.");
267
+ return 1;
268
+ }
269
+ }
246
270
  const transition = transitionMultiAgentV2(want, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
247
271
  if (!transition.ok) {
248
272
  log.error(`codex features ${want ? "enable" : "disable"} multi_agent_v2 failed: ${transition.error}`);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI-versus-proxy version skew (#2701).
3
+ *
4
+ * The reported failure: `ocx` on PATH is an older install than the running proxy, so its
5
+ * help describes commands the proxy does not have and its output describes a different
6
+ * build. Nothing surfaced that, because the CLI never compared the two.
7
+ *
8
+ * Kept in its own module rather than inside `status.ts` so `doctor` can reuse the exact
9
+ * comparison instead of reimplementing it -- two diagnostics disagreeing about whether an
10
+ * install is stale would be worse than neither reporting it.
11
+ */
12
+
13
+ /** Placeholder versions that mean "unknown", not "different". */
14
+ const PLACEHOLDERS = new Set(["unknown", "0.0.0"]);
15
+
16
+ export interface VersionSkew {
17
+ readonly cliVersion: string;
18
+ /** Version the live proxy reported, or null when nothing is live or it reported none. */
19
+ readonly proxyVersion: string | null;
20
+ readonly skewed: boolean;
21
+ /** Operator-facing explanation; null when there is nothing to report. */
22
+ readonly warning: string | null;
23
+ }
24
+
25
+ /**
26
+ * Compare the running CLI against the live proxy.
27
+ *
28
+ * Suppressed rather than reported when either side is a placeholder. `packageVersion()`
29
+ * answers `"unknown"` when `package.json` carries no string version, and the server's
30
+ * `VERSION` falls back to `"0.0.0"` when it cannot resolve its own package -- comparing
31
+ * against either would report skew that says nothing about the install. A false stale-CLI
32
+ * warning would send an operator to reinstall a healthy setup.
33
+ */
34
+ export function computeVersionSkew(cliVersion: string, proxyVersion: string | undefined): VersionSkew {
35
+ const proxy = proxyVersion ?? null;
36
+ if (proxy === null || PLACEHOLDERS.has(proxy) || PLACEHOLDERS.has(cliVersion) || proxy === cliVersion) {
37
+ return { cliVersion, proxyVersion: proxy, skewed: false, warning: null };
38
+ }
39
+ return {
40
+ cliVersion,
41
+ proxyVersion: proxy,
42
+ skewed: true,
43
+ warning: `CLI ${cliVersion} does not match the running proxy ${proxy} — this ocx on PATH is stale. `
44
+ + "Its help and features describe a different build. Reinstall, or run the proxy's own binary.",
45
+ };
46
+ }