@oxygen-agent/cli 1.1011.855 → 1.1011.974
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 +11 -0
- package/dist/functions-commands.js +1 -1
- package/dist/index.js +179 -132
- package/dist/workspace-utility-flags.d.ts +7 -0
- package/dist/workspace-utility-flags.js +45 -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.js +135 -4
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +10 -8
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
- 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/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/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/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/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/package.json +1 -1
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",
|
|
@@ -105,6 +108,8 @@ const MUTATING_COMMANDS = new Set([
|
|
|
105
108
|
"managed-inboxes recover",
|
|
106
109
|
"mailboxes warmup status",
|
|
107
110
|
"mailboxes warmup reconnect",
|
|
111
|
+
// Immediately moves a provider proxy and may pause or resume the sender.
|
|
112
|
+
"senders proxy-country",
|
|
108
113
|
// A noun leaf with its own action (not a group), so the default would
|
|
109
114
|
// advertise a STANDING per-viewer paid-enrichment toggle as a read. `enrichment`
|
|
110
115
|
// itself is not a MUTATING_VERBS entry because the other two `enrichment`
|
|
@@ -182,6 +187,10 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
|
182
187
|
// one. Keep the exception exact so genuine `run` actions remain mutations.
|
|
183
188
|
const READ_ONLY_COMMANDS = new Set([
|
|
184
189
|
"workflows run",
|
|
190
|
+
// `history` is a mutating verb for `whatsapp history` (it backfills a chat);
|
|
191
|
+
// these two only read a row's and a cell's recorded changes.
|
|
192
|
+
"rows history",
|
|
193
|
+
"cells history",
|
|
185
194
|
// Inspect existing creator receipts; never generate or import again.
|
|
186
195
|
"ugc posts draft-status",
|
|
187
196
|
"ugc posts history",
|
|
@@ -190,6 +199,8 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
190
199
|
// invocation still previews, so machine-readable discovery must not imply that
|
|
191
200
|
// calling the command without its confirmation flag writes anything.
|
|
192
201
|
const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
202
|
+
"deliverability placement-test run",
|
|
203
|
+
"deliverability placement-test send",
|
|
193
204
|
"managed-inboxes recover",
|
|
194
205
|
// /api/cli/find/run treats an omitted mode as dry_run for every Find capability.
|
|
195
206
|
"find email",
|
|
@@ -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.")
|