@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.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +3 -1
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +326 -111
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
- package/node_modules/@oxygen/shared/dist/geo.js +314 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +88 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +15 -5
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
package/README.md
CHANGED
|
@@ -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. */
|
package/dist/command-manifest.js
CHANGED
|
@@ -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
|
|
167
|
-
" external writes require --approved
|
|
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
|
/**
|
package/dist/http-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-client.js
CHANGED
|
@@ -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
|
|
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
|
-
...
|
|
126
|
+
...recovery,
|
|
121
127
|
},
|
|
122
|
-
exitCode:
|
|
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
|
-
...
|
|
136
|
+
...recovery,
|
|
131
137
|
},
|
|
132
|
-
exitCode:
|
|
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
|
-
|
|
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
|
-
...
|
|
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 (
|
|
187
|
+
if (!readOnly)
|
|
177
188
|
recordMutatingRequestAnswered();
|
|
178
189
|
throw error;
|
|
179
190
|
}
|
|
180
|
-
if (
|
|
191
|
+
if (!readOnly)
|
|
181
192
|
recordMutatingRequestAnswered();
|
|
182
193
|
return envelope.data;
|
|
183
194
|
}
|