@oxygen-agent/cli 1.1011.974 → 1.1012.2

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.d.ts +3 -1
  3. package/dist/command-manifest.js +29 -0
  4. package/dist/crm-support-flags.d.ts +1 -0
  5. package/dist/crm-support-flags.js +5 -1
  6. package/dist/customer-operation-commands.js +30 -1
  7. package/dist/help.js +10 -2
  8. package/dist/http-client.d.ts +4 -0
  9. package/dist/http-client.js +21 -10
  10. package/dist/index.js +326 -111
  11. package/dist/sender-limits-flags.d.ts +7 -0
  12. package/dist/sender-limits-flags.js +13 -0
  13. package/dist/transcript.js +1 -1
  14. package/node_modules/@oxygen/formula/dist/expression.js +16 -0
  15. package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
  16. package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
  17. package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
  19. package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
  20. package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
  22. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
  23. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
  24. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +5 -5
  27. package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  31. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
  34. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
  35. package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
  36. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
  37. package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
  38. package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
  39. package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
  40. package/node_modules/@oxygen/shared/dist/geo.js +314 -0
  41. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
  42. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
  44. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
  45. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
  46. package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
  47. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
  48. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
  50. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
  51. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
  52. package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
  53. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
  54. package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
  55. package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
  56. package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
  57. package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
  58. package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
  59. package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
  60. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  61. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  62. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  63. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  64. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +88 -0
  65. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -0
  66. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
  67. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
  68. package/node_modules/@oxygen/shared/package.json +15 -5
  69. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
  70. package/package.json +1 -1
  71. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
  72. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1011.974
37
+ Version: 1.1012.2
@@ -17,6 +17,7 @@ export type CommandManifestEntry = {
17
17
  flags: CommandManifestFlag[];
18
18
  spends_credits: boolean;
19
19
  mutates: boolean;
20
+ destructive: boolean;
20
21
  preview_by_default: boolean;
21
22
  json_supported: boolean;
22
23
  hidden: boolean;
@@ -33,10 +34,11 @@ export type CommandSearchResult = {
33
34
  total_matches: number;
34
35
  returned: number;
35
36
  route: Record<string, unknown> | null;
36
- commands: Array<Pick<CommandManifestEntry, "name" | "group" | "description" | "spends_credits" | "mutates" | "json_supported" | "hidden"> & {
37
+ commands: Array<Pick<CommandManifestEntry, "name" | "group" | "description" | "spends_credits" | "mutates" | "destructive" | "json_supported" | "hidden"> & {
37
38
  score: number;
38
39
  }>;
39
40
  hydrate: string;
41
+ next_step?: string;
40
42
  };
41
43
  export declare function buildCommandManifest(program: Command, binaryName: string): CommandManifest;
42
44
  /** Bounded natural-language projection over the complete local manifest. */
@@ -104,10 +104,23 @@ const MUTATING_VERBS = new Set([
104
104
  // status persists state and can pause the provider; reconnect replaces its
105
105
  // connection and can restart provider history. Other status commands are reads.
106
106
  const MUTATING_COMMANDS = new Set([
107
+ // These balance reads can post an eligible due grant or expire due sponsored
108
+ // credits. The exact exceptions disclose writes without inventing spend.
109
+ "billing balance",
110
+ "billing seats",
111
+ // The ledger audit reads that same balance first, with the same conditional writes.
112
+ "billing audit",
113
+ // Creates an approval or Checkout link; a first plan's Checkout can start the
114
+ // first-month offer window. The plan changes only after an admin confirms.
115
+ "billing change",
116
+ // --approved records a domain's renewal exit, or with --revoke resumes renewals.
117
+ "managed-inboxes exit",
107
118
  // A default preview is free; approval restores provider seats and billing.
108
119
  "managed-inboxes recover",
109
120
  "mailboxes warmup status",
110
121
  "mailboxes warmup reconnect",
122
+ // Approval creates a provider consent link; status can attach its receipt.
123
+ "mailboxes warmup google",
111
124
  // Immediately moves a provider proxy and may pause or resume the sender.
112
125
  "senders proxy-country",
113
126
  // A noun leaf with its own action (not a group), so the default would
@@ -134,6 +147,10 @@ const MUTATING_COMMANDS = new Set([
134
147
  // mode, but a small number of approved operations are explicitly
135
148
  // zero-credit. Keep those exceptions exact so discovery never invents spend.
136
149
  const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
150
+ // The compatibility --approved flag never bypasses canonical proposal review.
151
+ // Wiki revisions and canonical pin changes are free (Knowledge audit, 2026-10-06).
152
+ "knowledge page upsert",
153
+ "knowledge page pin",
137
154
  "inbox delete-message",
138
155
  "inbox forget-whatsapp-chat",
139
156
  "tables watcher preview",
@@ -168,12 +185,18 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
168
185
  // progress are covered by the existing monthly mailbox commitment.
169
186
  "mailboxes connect-oauth",
170
187
  "mailboxes sync",
188
+ // Separate Google consent always has a zero-credit ceiling; enrollment bills
189
+ // through its own warmup enable command.
190
+ "mailboxes warmup google",
171
191
  // Previews without --approved; with it, removes a program no creator ever
172
192
  // joined. No provider call, no credits.
173
193
  "ugc programs delete",
174
194
  // This approval only reopens exact fingerprint-bound tenant rows. It never
175
195
  // calls the provider, dispatches an action, or touches the credit ledger.
176
196
  "sequences recover-capacity",
197
+ // Provider reads are free; approval only records an exact match locally.
198
+ // Neither branch resends or writes to the provider.
199
+ "sequences reconcile",
177
200
  // Credential replacement is destructive but does not execute the graph,
178
201
  // call a provider, or spend Oxygen credits.
179
202
  "workflows webhooks rotate",
@@ -199,6 +222,8 @@ const READ_ONLY_COMMANDS = new Set([
199
222
  // invocation still previews, so machine-readable discovery must not imply that
200
223
  // calling the command without its confirmation flag writes anything.
201
224
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
225
+ "mailboxes warmup enable",
226
+ "mailboxes warmup google",
202
227
  "deliverability placement-test run",
203
228
  "deliverability placement-test send",
204
229
  "managed-inboxes recover",
@@ -233,6 +258,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
233
258
  // No action reads progress; an action previews until --approved.
234
259
  "mailboxes sync",
235
260
  "sequences recover-capacity",
261
+ "sequences reconcile",
236
262
  "support admin done",
237
263
  "support admin reply",
238
264
  // A bare call reads the Plain workspace and prints the plan; --apply writes.
@@ -282,11 +308,13 @@ export function searchCommandManifest(manifest, query, requestedLimit = DEFAULT_
282
308
  description: command.description,
283
309
  spends_credits: command.spends_credits,
284
310
  mutates: command.mutates,
311
+ destructive: command.destructive,
285
312
  json_supported: command.json_supported,
286
313
  hidden: command.hidden,
287
314
  score,
288
315
  })),
289
316
  hydrate: `${manifest.binary} commands get <exact-command> --json`,
317
+ ...(matches.length ? {} : { next_step: `No command matched. Describe the outcome with ${manifest.binary} capabilities search, or inspect ${manifest.binary} commands --json.` }),
290
318
  };
291
319
  }
292
320
  /** Exact second hop after search; avoids serializing the full command grammar. */
@@ -408,6 +436,7 @@ function toManifestEntry(command, path) {
408
436
  && (MUTATING_COMMANDS.has(commandName)
409
437
  || MUTATING_VERBS.has(leafVerb)
410
438
  || MUTATING_VERBS.has(verbPrefix))),
439
+ destructive: operations.some((spec) => spec.mcp.annotations?.destructiveHint === true),
411
440
  preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
412
441
  PREVIEW_BY_DEFAULT_COMMANDS.has(commandName),
413
442
  json_supported: flagStrings.some((flags) => flags.includes("--json")),
@@ -8,4 +8,5 @@ export declare function readCrmPhotoFields(options: {
8
8
  }): Record<string, unknown>;
9
9
  export declare function readCrmFileLabel(options: object): Record<string, unknown>;
10
10
  export declare function buildCrmObjectsPath(options: object): string;
11
+ export declare function buildCrmRecordPath(object: string, rowId: string, options: object): string;
11
12
  export declare function buildCrmConnectionsPath(object: string, rowId: string, options: object): string;
@@ -1,5 +1,5 @@
1
1
  import { OxygenError } from "@oxygen/shared/cli-result";
2
- import { CRM_CONNECTIONS_QUERY_CONTRACT, CRM_NOTE_EDIT_CONTRACT, CRM_OBJECTS_QUERY_CONTRACT, CRM_PHOTOS_CONTRACT, CRM_FILE_COMPLETE_CONTRACT } from "@oxygen/shared/contracts/crm-support";
2
+ import { CRM_CONNECTIONS_QUERY_CONTRACT, CRM_NOTE_EDIT_CONTRACT, CRM_OBJECTS_QUERY_CONTRACT, CRM_PHOTOS_CONTRACT, CRM_FILE_COMPLETE_CONTRACT, CRM_RECORD_QUERY_CONTRACT } from "@oxygen/shared/contracts/crm-support";
3
3
  import { validateContract } from "@oxygen/shared/contracts/schema";
4
4
  import { addContractFlags, readContractFlags } from "./contract-flags.js";
5
5
  import { assertModeFlagsExclusive } from "./cli-values.js";
@@ -19,6 +19,7 @@ function replaceFlags(command, attributes, contract, overlay) {
19
19
  export function registerCrmSupportFlags(program) {
20
20
  const crm = child(program, "crm");
21
21
  replaceFlags(child(crm, "objects"), [], CRM_OBJECTS_QUERY_CONTRACT);
22
+ replaceFlags(child(crm, "get"), [], CRM_RECORD_QUERY_CONTRACT);
22
23
  replaceFlags(child(crm, "connections"), ["conversationLimit", "sequenceLimit"], CRM_CONNECTIONS_QUERY_CONTRACT);
23
24
  replaceFlags(child(child(crm, "notes"), "update"), ["body", "title", "pin", "unpin"], CRM_NOTE_EDIT_CONTRACT, NOTE_OVERLAY);
24
25
  replaceFlags(child(crm, "photos"), ["limit", "live"], CRM_PHOTOS_CONTRACT);
@@ -55,6 +56,9 @@ function queryPath(path, query) {
55
56
  return params.size ? `${path}?${params}` : path;
56
57
  }
57
58
  export function buildCrmObjectsPath(options) { return queryPath("/api/cli/crm/objects", checkedFlags(options, CRM_OBJECTS_QUERY_CONTRACT)); }
59
+ export function buildCrmRecordPath(object, rowId, options) {
60
+ return queryPath(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}`, checkedFlags(options, CRM_RECORD_QUERY_CONTRACT));
61
+ }
58
62
  export function buildCrmConnectionsPath(object, rowId, options) {
59
63
  return queryPath(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/connections`, checkedFlags(options, CRM_CONNECTIONS_QUERY_CONTRACT));
60
64
  }
@@ -32,6 +32,10 @@ const GROUP_PURPOSES = {
32
32
  onboarding: "Save workspace discovery choices and continue onboarding.",
33
33
  };
34
34
  const OPERATION_FLAGS = {
35
+ "tables rows remove-selection": {
36
+ table: { help: "Table id or slug." },
37
+ row_ids: { help: "Comma-separated exact row IDs. Choose this, --filter-tree-json or --selection-json." },
38
+ },
35
39
  "tables favorite": { favorite: { negateHelp: "Unstar the selected Table or folder (favorite: false)." } },
36
40
  "tables favorites": { details: { negateHelp: "Return stored star keys without names or links (details: false)." } },
37
41
  };
@@ -92,7 +96,7 @@ export function registerCustomerOperationCommands(program, handle) {
92
96
  command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
93
97
  let input;
94
98
  try {
95
- input = readContractFlags(options, contract, overlay);
99
+ input = readContractFlags(ancestorConsumedOptions(command, selected, options), contract, overlay);
96
100
  if (spec.multipart && options.file !== undefined) {
97
101
  if (input[spec.multipart.contentField] !== undefined)
98
102
  throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
@@ -139,6 +143,31 @@ export function registerCustomerOperationCommands(program, handle) {
139
143
  }));
140
144
  }
141
145
  }
146
+ /**
147
+ * Commander lets an ancestor that declares the same flag consume it, so a value
148
+ * the user typed for this leaf lands on the parent and the leaf reads nothing:
149
+ * `sequences contacts rows --limit 1` sent no limit at all, because the native
150
+ * `sequences contacts` list owns `--limit` too, and the server returned its
151
+ * default page (OXP-1.11.67). Take such a value back only when the user typed it
152
+ * (value source `cli`); an ancestor's own default never becomes this leaf's input.
153
+ */
154
+ function ancestorConsumedOptions(leaf, selected, options) {
155
+ const merged = { ...options };
156
+ for (const option of leaf.options) {
157
+ const key = option.attributeName();
158
+ if (merged[key] !== undefined)
159
+ continue;
160
+ for (let ancestor = selected.parent; ancestor; ancestor = ancestor.parent) {
161
+ const owned = ancestor.options.find((candidate) => candidate.long === option.long);
162
+ if (!owned)
163
+ continue;
164
+ if (ancestor.getOptionValueSource(owned.attributeName()) === "cli")
165
+ merged[key] = ancestor.opts()[owned.attributeName()];
166
+ break;
167
+ }
168
+ }
169
+ return merged;
170
+ }
142
171
  function uploadContentType(filePath) {
143
172
  const types = {
144
173
  ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
package/dist/help.js CHANGED
@@ -141,6 +141,7 @@ export function applyOxygenHelp(program, binaryName) {
141
141
  "",
142
142
  "Agent quickstart:",
143
143
  ` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
144
+ ` ${binaryName} whoami --summary-json confirm the active workspace and identity (0 credits)`,
144
145
  // `login` auto-installs the skills, but an agent authenticating with
145
146
  // OXYGEN_API_KEY never runs it — without this step nothing on any
146
147
  // first-touch surface names the skill that teaches the GTM loops.
@@ -163,8 +164,9 @@ export function applyOxygenHelp(program, binaryName) {
163
164
  "Conventions:",
164
165
  " --json prints a stable envelope {ok, data|error, meta}; errors always print it.",
165
166
  " Free Sequence launch previews omit --approved and --max-credits.",
166
- " Credit-spending commands require --approved --max-credits <n>; zero-credit",
167
- " external writes require --approved plus the command's preview-bound plan token.",
167
+ " Credit-spending commands require --approved --max-credits <n>.",
168
+ " Zero-credit external writes require --approved; supply a preview-bound plan",
169
+ " token when the command requires one (see its preview and --help).",
168
170
  " State-changing responses include a web_url deep-link to inspect the result.",
169
171
  ].join("\n"));
170
172
  program.addHelpText("afterAll", `\nExit codes: ${exitCodeLine}`);
@@ -360,6 +362,12 @@ export function unknownCommandHint(program, argv) {
360
362
  const { command, path, next } = resolveCommandPath(program, argv);
361
363
  if (next === undefined || command.commands.length === 0)
362
364
  return null;
365
+ const identityExists = program.commands.some((candidate) => candidate.name() === "whoami");
366
+ const currentWorkspace = (command.parent === program && command.name() === "workspace" && next === "current")
367
+ || (command === program && next === "workspaces" && argv.some((token, index) => token === "workspaces" && argv[index + 1] === "current"));
368
+ if (identityExists && currentWorkspace) {
369
+ return `Hint: Active workspace and identity: \`${program.name()} whoami --summary-json\` (read-only, 0 credits).`;
370
+ }
363
371
  return buildUnknownCommandHint(command, path, next);
364
372
  }
365
373
  /**
@@ -6,6 +6,10 @@ type RequestOptions = {
6
6
  /** Stream a native authenticated download to an exclusively created file. */
7
7
  binaryOutputPath?: string;
8
8
  method?: "GET" | "POST" | "PATCH" | "DELETE";
9
+ /** A native read that uses POST for its selection payload. Never set on a write. */
10
+ readOnly?: boolean;
11
+ /** Only for audited preview routes that request no product mutation. */
12
+ previewOnly?: boolean;
9
13
  body?: Record<string, unknown>;
10
14
  formData?: FormData;
11
15
  credentials?: StoredCredentials;
@@ -11,6 +11,7 @@ const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
11
11
  // Replay on the Idempotency-Key is per route (most routes ignore the header),
12
12
  // so this must not promise that a same-key retry is safe.
13
13
  const UNCONFIRMED_WRITE_NEXT_STEP = "This change may already have been applied. Check the current state (for example with the matching list or get command) before you retry.";
14
+ const FAILED_READ_NEXT_STEP = "This read failed. Check your connection; it is safe to retry the same read.";
14
15
  const cliCompatibilityCheckKeys = new Set();
15
16
  export async function requestOxygen(// skipcq: JS-R1005
16
17
  path, options = {}) {
@@ -75,7 +76,13 @@ path, options = {}) {
75
76
  // echoed in error details so a re-run (OXYGEN_IDEMPOTENCY_KEY=<key>) can
76
77
  // retry without duplicating the write once the route supports replay.
77
78
  const method = options.method ?? "GET";
78
- const idempotencyKey = method === "GET"
79
+ const readOnly = method === "GET" || options.readOnly === true;
80
+ const recovery = {
81
+ next_step: options.previewOnly
82
+ ? "This was a preview; no changes were requested. Retry the preview when Oxygen is reachable."
83
+ : readOnly ? FAILED_READ_NEXT_STEP : UNCONFIRMED_WRITE_NEXT_STEP,
84
+ };
85
+ const idempotencyKey = readOnly
79
86
  ? undefined
80
87
  : options.idempotencyKey ?? process.env.OXYGEN_IDEMPOTENCY_KEY ?? randomUUID();
81
88
  if (idempotencyKey) {
@@ -108,7 +115,6 @@ path, options = {}) {
108
115
  catch (error) {
109
116
  if (error instanceof OxygenError)
110
117
  throw error;
111
- const unconfirmedWrite = method === "GET" ? {} : { next_step: UNCONFIRMED_WRITE_NEXT_STEP };
112
118
  if (timeout.timedOut() || (timeoutMs !== undefined && isAbortError(error))) {
113
119
  throw new OxygenError("network_timeout", "Oxygen API request timed out.", {
114
120
  details: {
@@ -117,9 +123,9 @@ path, options = {}) {
117
123
  timeout_ms: timeoutMs,
118
124
  trace_id: traceId,
119
125
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
120
- ...unconfirmedWrite,
126
+ ...recovery,
121
127
  },
122
- exitCode: 1,
128
+ exitCode: 8,
123
129
  });
124
130
  }
125
131
  throw new OxygenError("network_error", "Unable to reach Oxygen API.", {
@@ -127,9 +133,9 @@ path, options = {}) {
127
133
  api_url: apiUrl,
128
134
  reason: error instanceof Error ? error.message : "unknown",
129
135
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
130
- ...unconfirmedWrite,
136
+ ...recovery,
131
137
  },
132
- exitCode: 1,
138
+ exitCode: 5,
133
139
  });
134
140
  }
135
141
  finally {
@@ -141,12 +147,17 @@ path, options = {}) {
141
147
  // calling the gateway's page an invalid envelope.
142
148
  if (!(error instanceof OxygenError) || ![502, 503, 504].includes(response.status))
143
149
  throw error;
144
- throw new OxygenError("invalid_response", `Oxygen API did not answer in time (HTTP ${response.status}); the outcome is unknown.`, {
150
+ const message = options.previewOnly
151
+ ? `Oxygen API did not answer in time (HTTP ${response.status}); the preview was not returned.`
152
+ : readOnly
153
+ ? `Oxygen API did not answer in time (HTTP ${response.status}); the read was not returned.`
154
+ : `Oxygen API did not answer in time (HTTP ${response.status}); the outcome is unknown.`;
155
+ throw new OxygenError("invalid_response", message, {
145
156
  details: {
146
157
  status: response.status,
147
158
  trace_id: response.headers.get("x-oxygen-trace-id") ?? traceId,
148
159
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
149
- ...(method === "GET" ? {} : { next_step: UNCONFIRMED_WRITE_NEXT_STEP }),
160
+ ...recovery,
150
161
  },
151
162
  exitCode: 1,
152
163
  });
@@ -173,11 +184,11 @@ path, options = {}) {
173
184
  if (error.code === "cli_update_required")
174
185
  throw await recoverFromCliFloorRejection(error, endpoint);
175
186
  // Any other answer to a write may have written something; never re-run the command after it.
176
- if (method !== "GET")
187
+ if (!readOnly)
177
188
  recordMutatingRequestAnswered();
178
189
  throw error;
179
190
  }
180
- if (method !== "GET")
191
+ if (!readOnly)
181
192
  recordMutatingRequestAnswered();
182
193
  return envelope.data;
183
194
  }