@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.
- package/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -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
|
});
|
package/src/cli/usage-report.ts
CHANGED
|
@@ -38,10 +38,34 @@ interface UsageReportInput {
|
|
|
38
38
|
providers?: CostRow[];
|
|
39
39
|
days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
|
|
40
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
|
+
}[];
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
const MAX_MODEL_ROWS = 10;
|
|
44
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
|
+
|
|
45
69
|
function count(value: number | undefined): string {
|
|
46
70
|
return (value ?? 0).toLocaleString("en-US");
|
|
47
71
|
}
|
|
@@ -58,6 +82,8 @@ function usd(value: number | undefined): string {
|
|
|
58
82
|
|
|
59
83
|
function table(header: string[], rows: string[][]): string[] {
|
|
60
84
|
if (rows.length === 0) return [];
|
|
85
|
+
header = header.map(terminalText);
|
|
86
|
+
rows = rows.map(row => row.map(terminalText));
|
|
61
87
|
const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
|
|
62
88
|
const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
|
|
63
89
|
return [line(header), ...rows.map(line)];
|
|
@@ -68,7 +94,7 @@ function describeScope(data: UsageReportInput): string {
|
|
|
68
94
|
if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
|
|
69
95
|
if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
|
|
70
96
|
if (data.filter?.model) parts.push(`model=${data.filter.model}`);
|
|
71
|
-
return parts.join(", ");
|
|
97
|
+
return terminalText(parts.join(", "));
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
export function formatUsageReport(data: UsageReportInput): string[] {
|
|
@@ -78,7 +104,7 @@ export function formatUsageReport(data: UsageReportInput): string[] {
|
|
|
78
104
|
if (data.filter && !data.filter.matched) {
|
|
79
105
|
const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
|
|
80
106
|
.filter(Boolean).join(" and ");
|
|
81
|
-
lines.push(`No usage recorded for ${what} in this range.`);
|
|
107
|
+
lines.push(`No usage recorded for ${terminalText(what)} in this range.`);
|
|
82
108
|
lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
|
|
83
109
|
return lines;
|
|
84
110
|
}
|
|
@@ -110,6 +136,30 @@ export function formatUsageReport(data: UsageReportInput): string[] {
|
|
|
110
136
|
));
|
|
111
137
|
}
|
|
112
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
|
+
|
|
113
163
|
const models = (data.models ?? []).filter(row => row.requests > 0);
|
|
114
164
|
if (models.length > 0) {
|
|
115
165
|
lines.push("");
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,27 @@ import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
|
5
5
|
|
|
6
6
|
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Account log labels come in two families (#2699):
|
|
10
|
+
*
|
|
11
|
+
* - `p<hex6>` (plus the literal `main`) — a Codex pool account.
|
|
12
|
+
* - `o<hex6>` — a non-Codex OAuth provider account (xai, cursor, and siblings).
|
|
13
|
+
*
|
|
14
|
+
* Both are sha256-derived digests, never an email and never a raw provider account id. That is
|
|
15
|
+
* a privacy requirement, not a formatting preference: these labels are written to the usage log
|
|
16
|
+
* and served over the management API.
|
|
17
|
+
*
|
|
18
|
+
* hex6 is 16.7M values, so two accounts CAN collide and merge into one reported row. That is a
|
|
19
|
+
* reporting inaccuracy at operator scale, not a correctness or privacy failure, and it is the
|
|
20
|
+
* accepted cost of keeping the existing `p` format byte-compatible.
|
|
21
|
+
*/
|
|
22
|
+
export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^o[a-f0-9]{6}$/;
|
|
23
|
+
export const ACCOUNT_LOG_LABEL_RE = /^(?:main|[po][a-f0-9]{6})$/;
|
|
24
|
+
|
|
25
|
+
export function oauthAccountLogLabel(accountId: string, provider = ""): string {
|
|
26
|
+
return `o${createHash("sha256").update(`${provider}\0${accountId}`).digest("hex").slice(0, 6)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
export function createCodexAccountLogLabel(existingLabels: Iterable<string | undefined | null> = []): string {
|
|
9
30
|
const used = new Set([...existingLabels].filter((value): value is string => !!value));
|
|
10
31
|
for (let i = 0; i < 16; i++) {
|
|
@@ -46,6 +46,7 @@ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../
|
|
|
46
46
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
47
47
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
48
48
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
49
|
+
import { recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
49
50
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
50
51
|
import {
|
|
51
52
|
COMBO_NAMESPACE,
|
|
@@ -1330,6 +1331,9 @@ async function fetchProviderModelsWithAuth(
|
|
|
1330
1331
|
});
|
|
1331
1332
|
if (liveResult.ok) {
|
|
1332
1333
|
const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
|
|
1334
|
+
// Live Max-Mode evidence feeds the umbrella resolver's ultra gate
|
|
1335
|
+
// (devlog 260828_cursor_umbrella_catalog; union with static evidence).
|
|
1336
|
+
recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []);
|
|
1333
1337
|
const result = available.length > 0 ? available : configured;
|
|
1334
1338
|
// Cache the discovery-filtered roster without combo retention so a later
|
|
1335
1339
|
// gather can re-apply the current capture's retain set on read.
|
|
@@ -280,7 +280,16 @@ export interface CodexCoordinatorAdoptionOptions {
|
|
|
280
280
|
readonly onCheckpoint?: (checkpoint: CodexCoordinatorAdoptionCheckpoint) => void;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
function
|
|
283
|
+
function fsyncFile(path: string): void {
|
|
284
|
+
// Writable fd on purpose. Windows FlushFileBuffers needs GENERIC_WRITE;
|
|
285
|
+
// openSync(path, "r") is GENERIC_READ only and fails with EPERM (measured on
|
|
286
|
+
// Windows 11 25H2 / bun 1.3.14). prompt-journal.ts uses the same "r+" open.
|
|
287
|
+
const fd = openSync(path, "r+");
|
|
288
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function fsyncDir(path: string): void {
|
|
292
|
+
// POSIX directory fsync is O_RDONLY. O_RDWR ("r+") is EISDIR.
|
|
284
293
|
const fd = openSync(path, "r");
|
|
285
294
|
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
286
295
|
}
|
|
@@ -316,12 +325,12 @@ function publishAdoptionDatabase(
|
|
|
316
325
|
} finally {
|
|
317
326
|
temp.close();
|
|
318
327
|
}
|
|
319
|
-
|
|
328
|
+
fsyncFile(tempPath);
|
|
320
329
|
options.onCheckpoint?.("temp-committed");
|
|
321
330
|
|
|
322
331
|
linkSync(tempPath, finalDatabasePath);
|
|
323
332
|
options.onCheckpoint?.("published");
|
|
324
|
-
if (process.platform !== "win32")
|
|
333
|
+
if (process.platform !== "win32") fsyncDir(parent);
|
|
325
334
|
} catch (error) {
|
|
326
335
|
if (errorCode(error) !== "EEXIST") throw error;
|
|
327
336
|
} finally {
|
|
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
|
|
|
9
9
|
export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
|
|
10
10
|
schemaVersion: 1,
|
|
11
11
|
id: "openai.codex-forward.gpt-5-6-sol.responses",
|
|
12
|
-
version: "1.
|
|
12
|
+
version: "1.3.0",
|
|
13
13
|
subject: {
|
|
14
14
|
providerId: "openai",
|
|
15
15
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
@@ -109,5 +109,13 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
|
|
|
109
109
|
limitation: "The field is removed without inventing a replacement prompt_cache_options value.",
|
|
110
110
|
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-retention-removed"] }],
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
id: "prompt-cache-options",
|
|
114
|
+
feature: "request.prompt_cache_options",
|
|
115
|
+
disposition: "unsupported",
|
|
116
|
+
summary: "The ChatGPT Codex forward route does not receive public prompt cache options.",
|
|
117
|
+
limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
|
|
118
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-options-removed"] }],
|
|
119
|
+
},
|
|
112
120
|
],
|
|
113
121
|
} as const);
|