@oxygen-agent/cli 1.1011.855 → 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/admin-lead-magnet-commands.d.ts +7 -0
- package/dist/admin-lead-magnet-commands.js +128 -0
- 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/functions-commands.js +1 -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 +444 -208
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/workspace-utility-flags.d.ts +7 -0
- package/dist/workspace-utility-flags.js +45 -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/billing-anchors.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +7 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +279 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +21 -9
- 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 +13 -11
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
- 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/schema.js +10 -6
- package/node_modules/@oxygen/shared/dist/contracts/spintax.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/spintax.js +11 -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/contracts/workspace-utilities.d.ts +162 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.js +46 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.js +3 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +161 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +4 -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/partner-coupon-bonus.d.ts +45 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.js +57 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/table-filter-operators.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/table-filter-operators.js +50 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +4 -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 +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/index.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.d.ts +22 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.js +72 -0
- 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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type Handle = (command: string, options: {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
}, action: () => Promise<unknown>) => Promise<void>;
|
|
5
|
+
/** `oxygen admin lead-magnets`: the staff CMS for the public /resources catalog, the same rows /admin/lead-magnets edits. */
|
|
6
|
+
export declare function registerAdminLeadMagnetCommands(program: Command, handle: Handle): void;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, resolve } from "node:path";
|
|
3
|
+
import { Option } from "commander";
|
|
4
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
5
|
+
import { resolveLiveDryRunMode } from "./cli-values.js";
|
|
6
|
+
import { requestOxygen } from "./http-client.js";
|
|
7
|
+
// The resource JSON documented in docs/marketing-resources.md, mapped to the API body.
|
|
8
|
+
const FILE_FIELDS = {
|
|
9
|
+
slug: "slug", title: "title", description: "description", imageUrl: "image_url", imageAlt: "image_alt",
|
|
10
|
+
redirectUrl: "redirect_url", status: "status", sortOrder: "sort_order",
|
|
11
|
+
};
|
|
12
|
+
const COVER_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
13
|
+
// The server's own ceiling (LEAD_MAGNET_MAX_IMAGE_BYTES), checked first so an oversized file is never sent.
|
|
14
|
+
const MAX_COVER_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
const usageError = (message) => new OxygenError("invalid_request", message, { exitCode: 2 });
|
|
16
|
+
/** `oxygen admin lead-magnets`: the staff CMS for the public /resources catalog, the same rows /admin/lead-magnets edits. */
|
|
17
|
+
export function registerAdminLeadMagnetCommands(program, handle) {
|
|
18
|
+
const admin = program.commands.find((command) => command.name() === "admin");
|
|
19
|
+
if (!admin)
|
|
20
|
+
throw new Error("Admin commands must be registered first.");
|
|
21
|
+
const leadMagnets = admin
|
|
22
|
+
.command("lead-magnets")
|
|
23
|
+
.description("Publish, update and unpublish the email-gated lead magnets (resources) on the public /resources catalog. Staff only.");
|
|
24
|
+
leadMagnets
|
|
25
|
+
.command("list")
|
|
26
|
+
.description("List every lead magnet (resource) on /resources, drafts included, with status and public URL. Staff only, read-only.")
|
|
27
|
+
.option("--json", "Print a JSON envelope.")
|
|
28
|
+
.action((options) => handle("admin lead-magnets list", options, () => requestOxygen("/api/cli/admin/lead-magnets")));
|
|
29
|
+
leadMagnets
|
|
30
|
+
.command("get")
|
|
31
|
+
.description("Show one lead magnet (resource) by slug: every field, draft or published, and its public /resources URL. Staff only, read-only.")
|
|
32
|
+
.argument("<slug>", "The resource's slug (its /resources/<slug> path) or id.")
|
|
33
|
+
.option("--json", "Print a JSON envelope.")
|
|
34
|
+
.action((slug, options) => handle("admin lead-magnets get", options, () => requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`)));
|
|
35
|
+
leadMagnets
|
|
36
|
+
.command("upsert")
|
|
37
|
+
.description("Publish or update the lead magnet (resource) at a slug on /resources from a resource JSON file and/or flags; --cover uploads a local image. Dry run by default: prints the exact row and changed fields, uploads and writes nothing. --live applies it, and a published resource is public at once. Staff only.")
|
|
38
|
+
.option("--file <path>", "Resource JSON with slug, title, description, imageUrl, imageAlt, redirectUrl, status, sortOrder. Flags override its values.")
|
|
39
|
+
.option("--slug <slug>", "Permanent public slug (/resources/<slug>): lowercase letters, digits and hyphens. A new slug creates another resource.")
|
|
40
|
+
.option("--title <text>", "Title, up to 160 characters.")
|
|
41
|
+
.option("--description <text>", "Brief description, up to 600 characters.")
|
|
42
|
+
.option("--redirect-url <url>", "HTTPS link the visitor opens after entering an email.")
|
|
43
|
+
.option("--cover <path>", "Local PNG, JPEG or WebP cover image, up to 4 MB, uploaded to public storage on --live.")
|
|
44
|
+
.option("--image-url <url>", "HTTPS URL of an already hosted cover image, instead of --cover.")
|
|
45
|
+
.option("--image-alt <text>", "Cover alt text. Defaults to the title.")
|
|
46
|
+
.addOption(new Option("--status <status>", "published lists it on /resources; draft hides it. A new resource defaults to draft.").choices(["published", "draft"]))
|
|
47
|
+
.option("--sort-order <n>", "Catalog position, lowest first. Defaults to 0.")
|
|
48
|
+
.option("--dry-run", "Preview without uploading or writing (the default).")
|
|
49
|
+
.option("--live", "Upload the cover and write the resource.")
|
|
50
|
+
.option("--json", "Print a JSON envelope.")
|
|
51
|
+
.action((options) => handle("admin lead-magnets upsert", options, () => upsertLeadMagnet(options)));
|
|
52
|
+
leadMagnets
|
|
53
|
+
.command("unpublish")
|
|
54
|
+
.description("Unpublish a lead magnet (resource): set it to draft, which removes it and its page from /resources and keeps its email records. Dry run by default; --live applies it. Staff only.")
|
|
55
|
+
.argument("<slug>", "The resource's slug.")
|
|
56
|
+
.option("--dry-run", "Preview without writing (the default).")
|
|
57
|
+
.option("--live", "Unpublish it.")
|
|
58
|
+
.option("--json", "Print a JSON envelope.")
|
|
59
|
+
.action((slug, options) => handle("admin lead-magnets unpublish", options, () => requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: { status: "draft", mode: resolveLiveDryRunMode(options) },
|
|
62
|
+
})));
|
|
63
|
+
}
|
|
64
|
+
async function upsertLeadMagnet(options) {
|
|
65
|
+
const mode = resolveLiveDryRunMode(options);
|
|
66
|
+
if (options.cover !== undefined && options.imageUrl !== undefined) {
|
|
67
|
+
throw new OxygenError("conflicting_flags", "Pass either --cover or --image-url, not both.", { exitCode: 1 });
|
|
68
|
+
}
|
|
69
|
+
const fromFile = options.file === undefined ? {} : await readResourceFile(options.file);
|
|
70
|
+
if (options.slug !== undefined && fromFile.slug !== undefined && fromFile.slug !== options.slug) {
|
|
71
|
+
throw usageError(`--slug ${options.slug} does not match the file's slug ${String(fromFile.slug)}. The slug is the resource's identity: drop --slug or fix the file.`);
|
|
72
|
+
}
|
|
73
|
+
const flags = {
|
|
74
|
+
slug: options.slug, title: options.title, description: options.description, redirect_url: options.redirectUrl,
|
|
75
|
+
image_url: options.imageUrl, image_alt: options.imageAlt, status: options.status,
|
|
76
|
+
sort_order: options.sortOrder === undefined ? undefined : readSortOrder(options.sortOrder),
|
|
77
|
+
};
|
|
78
|
+
const { slug, ...fields } = { ...fromFile, ...Object.fromEntries(Object.entries(flags).filter(([, value]) => value !== undefined)) };
|
|
79
|
+
if (typeof slug !== "string" || !slug)
|
|
80
|
+
throw usageError("Name the resource with --slug, or a slug in --file.");
|
|
81
|
+
// A local cover replaces any imageUrl the file carries.
|
|
82
|
+
if (options.cover !== undefined)
|
|
83
|
+
delete fields.image_url;
|
|
84
|
+
const body = { ...fields, mode };
|
|
85
|
+
if (options.cover === undefined) {
|
|
86
|
+
return requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, { method: "POST", body });
|
|
87
|
+
}
|
|
88
|
+
const form = new FormData();
|
|
89
|
+
form.append("payload", JSON.stringify(body));
|
|
90
|
+
form.append("image", await readCover(options.cover));
|
|
91
|
+
return requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, { method: "POST", formData: form });
|
|
92
|
+
}
|
|
93
|
+
async function readResourceFile(path) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(await readFile(resolve(path), "utf8"));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw usageError(`--file must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
102
|
+
throw usageError("--file must hold one JSON object.");
|
|
103
|
+
const unknown = Object.keys(parsed).filter((key) => !Object.hasOwn(FILE_FIELDS, key));
|
|
104
|
+
if (unknown.length > 0) {
|
|
105
|
+
throw usageError(`--file has unknown fields: ${unknown.join(", ")}. A resource file has ${Object.keys(FILE_FIELDS).join(", ")}.`);
|
|
106
|
+
}
|
|
107
|
+
return Object.fromEntries(Object.entries(parsed).map(([key, value]) => [FILE_FIELDS[key], value]));
|
|
108
|
+
}
|
|
109
|
+
async function readCover(path) {
|
|
110
|
+
const type = COVER_TYPES[extname(path).toLowerCase()];
|
|
111
|
+
if (!type)
|
|
112
|
+
throw usageError("--cover must be a .png, .jpg, .jpeg or .webp image.");
|
|
113
|
+
let bytes;
|
|
114
|
+
try {
|
|
115
|
+
bytes = await readFile(resolve(path));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw usageError(`--cover could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
if (bytes.byteLength > MAX_COVER_BYTES)
|
|
121
|
+
throw usageError("--cover must be 4 MB or smaller.");
|
|
122
|
+
return new File([bytes], basename(path), { type });
|
|
123
|
+
}
|
|
124
|
+
function readSortOrder(value) {
|
|
125
|
+
if (!/^-?\d+$/.test(value.trim()))
|
|
126
|
+
throw usageError("--sort-order must be a whole number.");
|
|
127
|
+
return Number(value);
|
|
128
|
+
}
|
package/dist/command-manifest.js
CHANGED
|
@@ -78,6 +78,9 @@ const MUTATING_VERBS = new Set([
|
|
|
78
78
|
// Thread field, while a future `field-get` would be a read.
|
|
79
79
|
"field-set",
|
|
80
80
|
"file", "forward", "grant", "harvest", "history", "import", "insert", "interrupt", "invite",
|
|
81
|
+
// `crm keep` clears a seeded sample record's marker for good: from then on the
|
|
82
|
+
// record counts in pipeline totals. A verb that reads like "leave as is".
|
|
83
|
+
"keep",
|
|
81
84
|
"label-add", "label-remove", "launch", "log", "login", "logout", "mark-all-read", "mark-read", "materialize", "merge",
|
|
82
85
|
"migrate",
|
|
83
86
|
"move", "note", "off", "onboard", "order", "pause", "pin", "priority", "provision",
|
|
@@ -101,10 +104,18 @@ const MUTATING_VERBS = new Set([
|
|
|
101
104
|
// status persists state and can pause the provider; reconnect replaces its
|
|
102
105
|
// connection and can restart provider history. Other status commands are reads.
|
|
103
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",
|
|
104
111
|
// A default preview is free; approval restores provider seats and billing.
|
|
105
112
|
"managed-inboxes recover",
|
|
106
113
|
"mailboxes warmup status",
|
|
107
114
|
"mailboxes warmup reconnect",
|
|
115
|
+
// Approval creates a provider consent link; status can attach its receipt.
|
|
116
|
+
"mailboxes warmup google",
|
|
117
|
+
// Immediately moves a provider proxy and may pause or resume the sender.
|
|
118
|
+
"senders proxy-country",
|
|
108
119
|
// A noun leaf with its own action (not a group), so the default would
|
|
109
120
|
// advertise a STANDING per-viewer paid-enrichment toggle as a read. `enrichment`
|
|
110
121
|
// itself is not a MUTATING_VERBS entry because the other two `enrichment`
|
|
@@ -129,6 +140,10 @@ const MUTATING_COMMANDS = new Set([
|
|
|
129
140
|
// mode, but a small number of approved operations are explicitly
|
|
130
141
|
// zero-credit. Keep those exceptions exact so discovery never invents spend.
|
|
131
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",
|
|
132
147
|
"inbox delete-message",
|
|
133
148
|
"inbox forget-whatsapp-chat",
|
|
134
149
|
"tables watcher preview",
|
|
@@ -163,12 +178,18 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
|
163
178
|
// progress are covered by the existing monthly mailbox commitment.
|
|
164
179
|
"mailboxes connect-oauth",
|
|
165
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",
|
|
166
184
|
// Previews without --approved; with it, removes a program no creator ever
|
|
167
185
|
// joined. No provider call, no credits.
|
|
168
186
|
"ugc programs delete",
|
|
169
187
|
// This approval only reopens exact fingerprint-bound tenant rows. It never
|
|
170
188
|
// calls the provider, dispatches an action, or touches the credit ledger.
|
|
171
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",
|
|
172
193
|
// Credential replacement is destructive but does not execute the graph,
|
|
173
194
|
// call a provider, or spend Oxygen credits.
|
|
174
195
|
"workflows webhooks rotate",
|
|
@@ -182,6 +203,10 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
|
182
203
|
// one. Keep the exception exact so genuine `run` actions remain mutations.
|
|
183
204
|
const READ_ONLY_COMMANDS = new Set([
|
|
184
205
|
"workflows run",
|
|
206
|
+
// `history` is a mutating verb for `whatsapp history` (it backfills a chat);
|
|
207
|
+
// these two only read a row's and a cell's recorded changes.
|
|
208
|
+
"rows history",
|
|
209
|
+
"cells history",
|
|
185
210
|
// Inspect existing creator receipts; never generate or import again.
|
|
186
211
|
"ugc posts draft-status",
|
|
187
212
|
"ugc posts history",
|
|
@@ -190,6 +215,9 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
190
215
|
// invocation still previews, so machine-readable discovery must not imply that
|
|
191
216
|
// calling the command without its confirmation flag writes anything.
|
|
192
217
|
const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
218
|
+
"mailboxes warmup google",
|
|
219
|
+
"deliverability placement-test run",
|
|
220
|
+
"deliverability placement-test send",
|
|
193
221
|
"managed-inboxes recover",
|
|
194
222
|
// /api/cli/find/run treats an omitted mode as dry_run for every Find capability.
|
|
195
223
|
"find email",
|
|
@@ -222,6 +250,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
|
222
250
|
// No action reads progress; an action previews until --approved.
|
|
223
251
|
"mailboxes sync",
|
|
224
252
|
"sequences recover-capacity",
|
|
253
|
+
"sequences reconcile",
|
|
225
254
|
"support admin done",
|
|
226
255
|
"support admin reply",
|
|
227
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",
|
|
@@ -42,7 +42,7 @@ export function registerFunctionsCommands(program, handle) {
|
|
|
42
42
|
.requiredOption("--expected-draft-hash <hash>", "function.draft.contentHash returned by draft or describe; rejects concurrent edits.")
|
|
43
43
|
.option("--json", "Print a JSON envelope.")
|
|
44
44
|
.action((ref, options) => handle("functions publish", options, () => requestOxygen("/api/cli/functions", { method: "POST", body: { operation: "publish", function: ref, expected_draft_hash: options.expectedDraftHash } })));
|
|
45
|
-
functions.command("runs <function>").description("Inspect a Function's latest 100 invocations, the table that called each one, and their captured versions. For a call that did not complete, creditsUsed can show the estimate; `table-runs provider-summary <run_id>` reports the credits actually captured.")
|
|
45
|
+
functions.command("runs <function>").description("Inspect a Function's latest 100 invocations, the table that called each one, and their captured versions and bindings: each run's overwrite policy, output mapping and run condition as they were when it ran. Per row, the calling table's Function cell records overwrite_policy, written_columns, overwritten_columns (filled cells it replaced) and kept_columns (filled cells empty_only left). For a call that did not complete, creditsUsed can show the estimate; `table-runs provider-summary <run_id>` reports the credits actually captured.")
|
|
46
46
|
.option("--json", "Print a JSON envelope.")
|
|
47
47
|
.action((ref, options) => handle("functions runs", options, () => requestOxygen(`/api/cli/functions/runs?${new URLSearchParams({ function: ref })}`)));
|
|
48
48
|
functions.command("rename <function> <name>").description("Rename a Function while preserving its identity, callers and published execution.")
|
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
|
}
|