@oxygen-agent/cli 1.1010.721 → 1.1010.950

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 (118) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +14 -0
  5. package/dist/contract-flags.d.ts +42 -0
  6. package/dist/contract-flags.js +220 -0
  7. package/dist/credentials.d.ts +2 -0
  8. package/dist/credentials.js +6 -3
  9. package/dist/functions-commands.js +1 -1
  10. package/dist/http-client.js +28 -4
  11. package/dist/index.js +622 -325
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/sender-limits-flags.d.ts +14 -0
  15. package/dist/sender-limits-flags.js +157 -0
  16. package/dist/streamed-file-import.d.ts +58 -0
  17. package/dist/streamed-file-import.js +115 -0
  18. package/dist/ugc-commands.js +16 -2
  19. package/dist/update.d.ts +29 -0
  20. package/dist/update.js +62 -16
  21. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  22. package/dist/workflow-plan-limit-notices.js +28 -0
  23. package/node_modules/@oxygen/cli-ugc/dist/commands.js +6 -6
  24. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +4 -0
  25. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +20 -8
  26. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +1 -0
  27. package/node_modules/@oxygen/cli-ugc/dist/index.js +1 -0
  28. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
  29. package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
  30. package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
  31. package/node_modules/@oxygen/shared/dist/billing.js +333 -42
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
  33. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +39 -0
  34. package/node_modules/@oxygen/shared/dist/contracts/operations.js +11 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +186 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/schema.js +260 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +67 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +88 -0
  39. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  40. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  41. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  42. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  43. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  44. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  45. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  46. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  47. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  48. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  49. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
  50. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
  51. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  52. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  53. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  54. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  55. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  56. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  57. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  58. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  59. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  60. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  61. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
  62. package/node_modules/@oxygen/shared/dist/index.js +2 -42
  63. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +11 -0
  64. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +41 -0
  65. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  66. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  67. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  68. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  69. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
  70. package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
  71. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  72. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  73. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  74. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  75. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
  76. package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
  77. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
  78. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
  79. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
  80. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
  81. package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +4 -2
  82. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  83. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  84. package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
  85. package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
  86. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  87. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  88. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
  89. package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
  90. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  91. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  92. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  93. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  94. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
  95. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
  96. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +35 -5
  97. package/node_modules/@oxygen/shared/dist/table-capacity.js +40 -8
  98. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  99. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  100. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  101. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  102. package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
  103. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  104. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  105. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  106. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  107. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  108. package/node_modules/@oxygen/shared/dist/version.js +17 -27
  109. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  110. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  111. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  112. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  113. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  114. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  115. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  116. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  117. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  118. package/package.json +1 -1
@@ -0,0 +1,220 @@
1
+ // The CLI half of a shared resource contract (@oxygen/shared/contracts/schema).
2
+ // A command's flags and the request body they build are generated from the same
3
+ // contract that generates the MCP input schema and validates the route, so a
4
+ // settable field cannot exist on one surface and not the other.
5
+ //
6
+ // Derivation (the overlay only renames, re-words, aliases or hides):
7
+ // scalar --<kebab-name> <value> numbers parsed and range-checked
8
+ // enum --<kebab-name> <value> membership checked
9
+ // boolean --<x> / --no-<x> tri-state: absent = not sent
10
+ // array of scalars --<kebab-name> <a,b,c>
11
+ // object, <= 6 scalar fields --<obj>-<field> per field
12
+ // larger object --<obj>-json <json>
13
+ // nullable also --clear-<x>, which sends null
14
+ // Every value is checked inside the action (never in a Commander parser), so a
15
+ // bad value comes back as the JSON error envelope, not a raw stack trace.
16
+ import { Option } from "commander";
17
+ import { OxygenError } from "@oxygen/shared/cli-result";
18
+ import { unwrapNullable } from "@oxygen/shared/contracts/schema";
19
+ const MAX_SPLAT_FIELDS = 6;
20
+ function kebab(name) {
21
+ return name.replace(/_/g, "-");
22
+ }
23
+ function attribute(flag) {
24
+ return flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
25
+ }
26
+ function isObject(property) {
27
+ return "type" in property && property.type === "object" && property.properties !== undefined;
28
+ }
29
+ function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = "") {
30
+ const specs = [];
31
+ for (const [name, raw] of Object.entries(properties)) {
32
+ const path = [...parentPath, name];
33
+ const entry = overlay[path.join(".")] ?? {};
34
+ if (entry.omit)
35
+ continue;
36
+ const { property, nullable } = unwrapNullable(raw);
37
+ const flag = entry.flag ?? `${parentFlag}${kebab(name)}`;
38
+ if (isObject(property)) {
39
+ const fields = Object.values(property.properties ?? {});
40
+ const splat = entry.flatten || (fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
41
+ if (splat)
42
+ specs.push(...contractFlagSpecs(property.properties ?? {}, overlay, path, entry.flatten ? "" : `${flag}-`));
43
+ else
44
+ specs.push({ kind: "json", path, flag: `${flag}-json`, entry });
45
+ }
46
+ else if ("type" in property && property.type === "boolean") {
47
+ specs.push(entry.pair ? { kind: "pair", path, on: entry.pair.on, off: entry.pair.off, entry } : { kind: "boolean", path, flag, entry });
48
+ }
49
+ else {
50
+ specs.push({ kind: "value", path, flag, property, entry });
51
+ }
52
+ if (nullable && !entry.noClear)
53
+ specs.push({ kind: "clear", path, flag: `clear-${flag}`, target: flag });
54
+ }
55
+ return specs;
56
+ }
57
+ function helpFor(spec, property) {
58
+ if (spec.entry.help !== undefined)
59
+ return spec.entry.help;
60
+ const description = property && "description" in property ? property.description ?? "" : "";
61
+ const values = property && "enum" in property && property.enum ? ` One of: ${property.enum.join(", ")}.` : "";
62
+ return `${description}${values}`.trim();
63
+ }
64
+ function propertyAt(contract, path) {
65
+ let current = contract;
66
+ for (const key of path) {
67
+ const unwrapped = current ? unwrapNullable(current).property : undefined;
68
+ current = unwrapped && isObject(unwrapped) ? unwrapped.properties?.[key] : undefined;
69
+ }
70
+ return current ? unwrapNullable(current).property : undefined;
71
+ }
72
+ /** Registers the contract's flags on `command`, in contract order. */
73
+ export function addContractFlags(command, contract, overlay = {}) {
74
+ for (const spec of contractFlagSpecs(contract.properties, overlay)) {
75
+ const property = "path" in spec ? propertyAt(contract, spec.path) : undefined;
76
+ if (spec.kind === "pair") {
77
+ command.addOption(new Option(`--${spec.on}`, spec.entry.pair?.onHelp));
78
+ command.addOption(new Option(`--${spec.off}`, spec.entry.pair?.offHelp));
79
+ continue;
80
+ }
81
+ if (spec.kind === "clear") {
82
+ command.addOption(new Option(`--${spec.flag}`, `Clear it (sends null). Not with --${spec.target}.`));
83
+ continue;
84
+ }
85
+ if (spec.kind === "boolean") {
86
+ command.addOption(new Option(`--${spec.flag}`, helpFor(spec, property)));
87
+ command.addOption(new Option(`--no-${spec.flag}`, spec.entry.negateHelp ?? `Sends ${spec.path.join(".")}: false.`));
88
+ continue;
89
+ }
90
+ const valueName = spec.entry.valueName ?? (spec.kind === "json" ? "<json>" : "<value>");
91
+ command.addOption(new Option(`--${spec.flag} ${valueName}`, helpFor(spec, property)));
92
+ for (const alias of spec.entry.aliases ?? [])
93
+ command.addOption(new Option(`--${alias} ${valueName}`).hideHelp());
94
+ }
95
+ return command;
96
+ }
97
+ function invalid(flag, value, message, code = "invalid_number") {
98
+ return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 1 });
99
+ }
100
+ function parseScalar(property, raw, flag) {
101
+ if (!("type" in property))
102
+ return raw;
103
+ if (property.type === "integer" || property.type === "number") {
104
+ const parsed = Number(raw);
105
+ const whole = property.type === "integer";
106
+ const minimum = property.minimum;
107
+ const ok = whole ? Number.isSafeInteger(parsed) : Number.isFinite(parsed);
108
+ if (!ok || (minimum !== undefined && parsed < minimum)) {
109
+ throw invalid(flag, raw, minimum === 0
110
+ ? `Expected a non-negative ${whole ? "integer" : "number"}.`
111
+ : `Expected ${whole ? "an integer" : "a number"}${minimum !== undefined ? ` of at least ${minimum}` : ""}.`);
112
+ }
113
+ // A clamped field is lowered (and reported) by the server; anything else over the maximum is refused here.
114
+ if (property.maximum !== undefined && parsed > property.maximum && !property["x-clamp"]) {
115
+ throw invalid(flag, raw, `Expected at most ${property.maximum}.`);
116
+ }
117
+ return parsed;
118
+ }
119
+ if (property.type === "string" && property.enum && !property.enum.includes(raw)) {
120
+ throw invalid(flag, raw, `--${flag} must be one of: ${property.enum.join(", ")}.`, "invalid_option");
121
+ }
122
+ return raw;
123
+ }
124
+ function readValue(spec, raw) {
125
+ if (spec.entry.parse)
126
+ return spec.entry.parse(raw);
127
+ const property = spec.property;
128
+ if ("type" in property && property.type === "array") {
129
+ return raw.split(",").map((part) => part.trim()).filter(Boolean)
130
+ .map((part) => parseScalar(unwrapNullable(property.items).property, part, spec.flag));
131
+ }
132
+ return parseScalar(property, raw, spec.flag);
133
+ }
134
+ function setPath(body, path, value) {
135
+ let cursor = body;
136
+ for (const key of path.slice(0, -1)) {
137
+ const next = cursor[key];
138
+ if (typeof next === "object" && next !== null && !Array.isArray(next))
139
+ cursor = next;
140
+ else
141
+ cursor = (cursor[key] = {});
142
+ }
143
+ cursor[path[path.length - 1]] = value;
144
+ }
145
+ function stringOption(options, flag, aliases = []) {
146
+ for (const name of [flag, ...aliases]) {
147
+ const value = options[attribute(name)];
148
+ if (typeof value === "string" && value.trim())
149
+ return value.trim();
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * The request body the parsed flags describe: only the fields given, nested as
155
+ * the contract nests them. An object none of whose flags was given is absent.
156
+ */
157
+ export function readContractFlags(options, contract, overlay = {}) {
158
+ const body = {};
159
+ const specs = contractFlagSpecs(contract.properties, overlay);
160
+ const given = new Set();
161
+ for (const spec of specs) {
162
+ if (spec.kind === "value") {
163
+ const raw = stringOption(options, spec.flag, spec.entry.aliases);
164
+ if (raw === undefined)
165
+ continue;
166
+ setPath(body, spec.path, readValue(spec, raw));
167
+ given.add(spec.flag);
168
+ }
169
+ else if (spec.kind === "json") {
170
+ const raw = stringOption(options, spec.flag, spec.entry.aliases);
171
+ if (raw === undefined)
172
+ continue;
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(raw);
176
+ }
177
+ catch {
178
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
179
+ }
180
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
181
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
182
+ }
183
+ setPath(body, spec.path, parsed);
184
+ given.add(spec.flag);
185
+ }
186
+ else if (spec.kind === "boolean") {
187
+ const value = options[attribute(spec.flag)];
188
+ if (typeof value === "boolean")
189
+ setPath(body, spec.path, value);
190
+ }
191
+ else if (spec.kind === "pair") {
192
+ if (options[attribute(spec.off)] === true)
193
+ setPath(body, spec.path, false);
194
+ else if (options[attribute(spec.on)] === true)
195
+ setPath(body, spec.path, true);
196
+ }
197
+ else if (options[attribute(spec.flag)] === true) {
198
+ if (given.has(spec.target)) {
199
+ throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 1 });
200
+ }
201
+ setPath(body, spec.path, null);
202
+ }
203
+ }
204
+ return body;
205
+ }
206
+ /** Every long flag `addContractFlags` registers, in order (for tests and docs). */
207
+ export function contractFlagNames(contract, overlay = {}) {
208
+ return contractFlagSpecs(contract.properties, overlay).flatMap((spec) => {
209
+ switch (spec.kind) {
210
+ case "pair":
211
+ return [`--${spec.on}`, `--${spec.off}`];
212
+ case "boolean":
213
+ return [`--${spec.flag}`, `--no-${spec.flag}`];
214
+ case "clear":
215
+ return [`--${spec.flag}`];
216
+ default:
217
+ return [`--${spec.flag}`, ...(spec.entry.aliases ?? []).map((alias) => `--${alias}`)];
218
+ }
219
+ });
220
+ }
@@ -77,4 +77,6 @@ export declare function pickProfileNameForUserSession(userId: string, candidateS
77
77
  export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
78
78
  export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
79
79
  export declare function normalizeApiUrl(value: string): string;
80
+ /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
81
+ export declare function resolveConfigDir(env?: NodeJS.ProcessEnv): string;
80
82
  export {};
@@ -411,12 +411,15 @@ function credentialStore(env) {
411
411
  return "credential-manager";
412
412
  return "file";
413
413
  }
414
- function credentialsPath(env) {
415
- const configDir = env.OXYGEN_CONFIG_DIR?.trim()
414
+ /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
415
+ export function resolveConfigDir(env = process.env) {
416
+ return env.OXYGEN_CONFIG_DIR?.trim()
416
417
  || (process.platform === "win32"
417
418
  ? join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
418
419
  : join(homedir(), ".config", "oxygen"));
419
- return join(configDir, "credentials.json");
420
+ }
421
+ function credentialsPath(env) {
422
+ return join(resolveConfigDir(env), "credentials.json");
420
423
  }
421
424
  function readFileCredentialPayload(env) {
422
425
  const path = credentialsPath(env);
@@ -47,7 +47,7 @@ export function registerFunctionsCommands(program, handle) {
47
47
  functions.command("rename <function> <name>").description("Rename a Function while preserving its identity, callers and published execution.")
48
48
  .option("--json", "Print a JSON envelope.")
49
49
  .action((ref, name, options) => handle("functions rename", options, () => requestOxygen("/api/cli/functions", { method: "POST", body: { operation: "rename", function: ref, display_name: name } })));
50
- functions.command("delete <function>").description("Preview removal from the Function library, then remove it with --yes; preserves backing tables, rows and run history. Connected callers or active runs block deletion.")
50
+ functions.command("delete <function>").description("Preview removal from the Function library, then remove it with --yes; preserves backing tables, rows and run history. The preview names every column that calls it: those stop running, keeping the values they already wrote. Only the Function's own active runs block deletion.")
51
51
  .addHelpText("after", "\nDeleting your own Function is final: there is no restore, unlike `tables delete`. To reuse the logic, `tables duplicate` the backing Table and draft a new Function from the copy.\n")
52
52
  .option("--yes", "Confirm removal of this Function after reviewing its callers.")
53
53
  .option("--json", "Print a JSON envelope.")
@@ -1,5 +1,6 @@
1
1
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
3
4
  import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
4
5
  import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
5
6
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
@@ -31,7 +32,14 @@ path, options = {}) {
31
32
  ...init, signal: init?.signal ? AbortSignal.any([options.signal, init.signal]) : options.signal,
32
33
  }) : baseFetch;
33
34
  if (path !== "/api/health" && shouldCheckCliCompatibility(options)) {
34
- await ensureFreshCliForApiUrl(apiUrl, { fetch: fetchImpl, endpoint });
35
+ try {
36
+ await ensureFreshCliForApiUrl(apiUrl, { fetch: fetchImpl, endpoint });
37
+ }
38
+ catch (error) {
39
+ // The preflight dialed only /api/health: nothing has run yet, so an
40
+ // automatic update may re-run this command in the new binary.
41
+ throw error instanceof OxygenError ? await recoverFromCliFloorRejection(error, endpoint) : error;
42
+ }
35
43
  }
36
44
  options.signal?.throwIfAborted();
37
45
  const traceId = options.traceId ?? randomUUID();
@@ -117,17 +125,33 @@ path, options = {}) {
117
125
  }
118
126
  const envelope = await readEnvelope(response);
119
127
  const compatibility = readEnvelopeCompatibility(envelope);
120
- assertCliMeetsMinimumApiVersion(compatibility, options, endpoint);
128
+ const failed = !response.ok || !envelope.ok;
129
+ try {
130
+ assertCliMeetsMinimumApiVersion(compatibility, options, endpoint);
131
+ }
132
+ catch (error) {
133
+ // Only a refused request is safe to re-run: a successful one already did its work.
134
+ throw failed && error instanceof OxygenError ? await recoverFromCliFloorRejection(error, endpoint) : error;
135
+ }
121
136
  warnIfCliIsOlderThanApi(compatibility.version, endpoint);
122
- if (!response.ok || !envelope.ok) {
137
+ if (failed) {
123
138
  const failure = envelope;
124
139
  const responseTraceId = response.headers.get("x-oxygen-trace-id") ?? traceId;
125
140
  const details = withRetryAfterDetails(failure.error.details, response);
126
- throw new OxygenError(failure.error.code, describeFailureAgainstEndpoint(failure.error.code, failure.error.message, endpoint), {
141
+ const error = new OxygenError(failure.error.code, describeFailureAgainstEndpoint(failure.error.code, failure.error.message, endpoint), {
127
142
  details: withTraceDetails(details, responseTraceId, compatibility, endpoint),
128
143
  exitCode: 1,
129
144
  });
145
+ // The floor gate refuses before the route does any work (assertCliClientVersionFloor).
146
+ if (error.code === "cli_update_required")
147
+ throw await recoverFromCliFloorRejection(error, endpoint);
148
+ // Any other answer to a write may have written something; never re-run the command after it.
149
+ if (method !== "GET")
150
+ recordMutatingRequestAnswered();
151
+ throw error;
130
152
  }
153
+ if (method !== "GET")
154
+ recordMutatingRequestAnswered();
131
155
  return envelope.data;
132
156
  }
133
157
  async function readBoundedBinaryResponse(response, maxBytes) {