@oxygen-agent/cli 1.1011.974 → 1.1011.1091
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.js +18 -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 +272 -83
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +146 -11
- 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 +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- 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/provider-balance-signal.js +3 -2
- 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/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -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 +5 -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
package/dist/command-manifest.js
CHANGED
|
@@ -104,10 +104,16 @@ 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",
|
|
107
111
|
// A default preview is free; approval restores provider seats and billing.
|
|
108
112
|
"managed-inboxes recover",
|
|
109
113
|
"mailboxes warmup status",
|
|
110
114
|
"mailboxes warmup reconnect",
|
|
115
|
+
// Approval creates a provider consent link; status can attach its receipt.
|
|
116
|
+
"mailboxes warmup google",
|
|
111
117
|
// Immediately moves a provider proxy and may pause or resume the sender.
|
|
112
118
|
"senders proxy-country",
|
|
113
119
|
// A noun leaf with its own action (not a group), so the default would
|
|
@@ -134,6 +140,10 @@ const MUTATING_COMMANDS = new Set([
|
|
|
134
140
|
// mode, but a small number of approved operations are explicitly
|
|
135
141
|
// zero-credit. Keep those exceptions exact so discovery never invents spend.
|
|
136
142
|
const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
143
|
+
// The compatibility --approved flag never bypasses canonical proposal review.
|
|
144
|
+
// Wiki revisions and canonical pin changes are free (Knowledge audit, 2026-10-06).
|
|
145
|
+
"knowledge page upsert",
|
|
146
|
+
"knowledge page pin",
|
|
137
147
|
"inbox delete-message",
|
|
138
148
|
"inbox forget-whatsapp-chat",
|
|
139
149
|
"tables watcher preview",
|
|
@@ -168,12 +178,18 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
|
168
178
|
// progress are covered by the existing monthly mailbox commitment.
|
|
169
179
|
"mailboxes connect-oauth",
|
|
170
180
|
"mailboxes sync",
|
|
181
|
+
// Separate Google consent always has a zero-credit ceiling; enrollment bills
|
|
182
|
+
// through its own warmup enable command.
|
|
183
|
+
"mailboxes warmup google",
|
|
171
184
|
// Previews without --approved; with it, removes a program no creator ever
|
|
172
185
|
// joined. No provider call, no credits.
|
|
173
186
|
"ugc programs delete",
|
|
174
187
|
// This approval only reopens exact fingerprint-bound tenant rows. It never
|
|
175
188
|
// calls the provider, dispatches an action, or touches the credit ledger.
|
|
176
189
|
"sequences recover-capacity",
|
|
190
|
+
// Provider reads are free; approval only records an exact match locally.
|
|
191
|
+
// Neither branch resends or writes to the provider.
|
|
192
|
+
"sequences reconcile",
|
|
177
193
|
// Credential replacement is destructive but does not execute the graph,
|
|
178
194
|
// call a provider, or spend Oxygen credits.
|
|
179
195
|
"workflows webhooks rotate",
|
|
@@ -199,6 +215,7 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
199
215
|
// invocation still previews, so machine-readable discovery must not imply that
|
|
200
216
|
// calling the command without its confirmation flag writes anything.
|
|
201
217
|
const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
218
|
+
"mailboxes warmup google",
|
|
202
219
|
"deliverability placement-test run",
|
|
203
220
|
"deliverability placement-test send",
|
|
204
221
|
"managed-inboxes recover",
|
|
@@ -233,6 +250,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
|
233
250
|
// No action reads progress; an action previews until --approved.
|
|
234
251
|
"mailboxes sync",
|
|
235
252
|
"sequences recover-capacity",
|
|
253
|
+
"sequences reconcile",
|
|
236
254
|
"support admin done",
|
|
237
255
|
"support admin reply",
|
|
238
256
|
// A bare call reads the Plain workspace and prints the plan; --apply writes.
|
|
@@ -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 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
|
}
|