@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.
- package/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- 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
|
});
|
|
@@ -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 —
|
|
92
|
-
: "multi_agent_v2: OFF —
|
|
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
|
|
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
|
-
|
|
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
|
-
?
|
|
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")
|
|
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
|
|
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
|
+
}
|