@oxygen-agent/cli 1.1014.1696 → 1.1014.1763
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-families/program.js +4 -0
- package/dist/command-manifest.js +14 -2
- package/dist/command-support.js +1 -1
- package/dist/commands/admin.js +82 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/copilot.js +29 -7
- package/dist/commands/domains.js +7 -0
- package/dist/commands/email-assets.d.ts +3 -0
- package/dist/commands/email-assets.js +78 -0
- package/dist/commands/email-templates.d.ts +3 -0
- package/dist/commands/email-templates.js +101 -0
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/sequence-start-at.d.ts +7 -0
- package/dist/sequence-start-at.js +21 -0
- package/dist/sequences-brand-kit-command.d.ts +28 -0
- package/dist/sequences-brand-kit-command.js +83 -0
- package/dist/sequences-content-commands.d.ts +28 -0
- package/dist/sequences-content-commands.js +176 -0
- package/dist/sequences-newsletter-commands.d.ts +6 -0
- package/dist/sequences-newsletter-commands.js +108 -0
- package/dist/sequences-test-send-command.d.ts +3 -0
- package/dist/sequences-test-send-command.js +47 -0
- package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
- package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
- package/node_modules/@oxygen/formula/dist/expression.js +26 -4
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +5 -7
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +174 -10
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +28 -2
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
- package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +24 -1
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +51 -4
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -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/workspace-event-catalog.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ import { registerDeliverabilityFamily } from "../commands/deliverability.js";
|
|
|
33
33
|
import { registerDirectoryFamily } from "../commands/directory.js";
|
|
34
34
|
import { registerDomainsFamily } from "../commands/domains.js";
|
|
35
35
|
import { registerEgressFamily } from "../commands/egress.js";
|
|
36
|
+
import { registerEmailAssetsFamily } from "../commands/email-assets.js";
|
|
37
|
+
import { registerEmailTemplatesFamily } from "../commands/email-templates.js";
|
|
36
38
|
import { registerEmailFamily } from "../commands/email.js";
|
|
37
39
|
import { registerEngagementFamily } from "../commands/engagement.js";
|
|
38
40
|
import { registerEnrichColumnFamily } from "../commands/enrich-column.js";
|
|
@@ -215,6 +217,8 @@ export function createProgram() {
|
|
|
215
217
|
registerVoiceFamily(program, handleAsyncAction);
|
|
216
218
|
registerSuppressionsFamily(program, handleAsyncAction);
|
|
217
219
|
registerSubscriptionsFamily(program, handleAsyncAction);
|
|
220
|
+
registerEmailAssetsFamily(program, handleAsyncAction);
|
|
221
|
+
registerEmailTemplatesFamily(program, handleAsyncAction);
|
|
218
222
|
registerEmailFamily(program, handleAsyncAction);
|
|
219
223
|
registerManagedInboxesFamily(program, handleAsyncAction);
|
|
220
224
|
registerMailboxesFamily(program, handleAsyncAction);
|
package/dist/command-manifest.js
CHANGED
|
@@ -99,7 +99,7 @@ const MUTATING_VERBS = new Set([
|
|
|
99
99
|
// `support admin todo` moves a Plain Thread back into the queue. The noun
|
|
100
100
|
// reads like a state, but the command is the transition into it.
|
|
101
101
|
"todo",
|
|
102
|
-
"unarchive", "unpublish", "unshare",
|
|
102
|
+
"unarchive", "unpublish", "unschedule", "unshare",
|
|
103
103
|
"unbind", "unsubscribe", "update", "upload", "upsert", "use", "warm-send", "warmup",
|
|
104
104
|
"withdraw", "write",
|
|
105
105
|
// `voice numbers release` returns a number to the carrier: irreversible,
|
|
@@ -109,7 +109,15 @@ const MUTATING_VERBS = new Set([
|
|
|
109
109
|
// Exact commands that must not inherit a read-only discovery label. Warmup
|
|
110
110
|
// status persists state and can pause the provider; reconnect replaces its
|
|
111
111
|
// connection and can restart provider history. Other status commands are reads.
|
|
112
|
+
// Exact paid leaves with no approval flag: a newsletter test send emails up to
|
|
113
|
+
// five member addresses at send.bulk_email each.
|
|
114
|
+
const SPENDING_WITHOUT_APPROVAL_COMMANDS = new Set([
|
|
115
|
+
"sequences test-send",
|
|
116
|
+
]);
|
|
112
117
|
const MUTATING_COMMANDS = new Set([
|
|
118
|
+
// A newsletter test send emails members and is metered; the hyphenated leaf
|
|
119
|
+
// matches no verb (`test` is not one).
|
|
120
|
+
"sequences test-send",
|
|
113
121
|
// Live DNS checks save observations; --cached explicitly reads saved evidence.
|
|
114
122
|
"domains dns",
|
|
115
123
|
// Creates or re-checks the Amazon SES identity and records its DKIM and
|
|
@@ -153,6 +161,9 @@ const MUTATING_COMMANDS = new Set([
|
|
|
153
161
|
"mailboxes warmup google",
|
|
154
162
|
// Immediately moves a provider proxy and may pause or resume the sender.
|
|
155
163
|
"senders proxy-country",
|
|
164
|
+
// Staff stop or restart a workspace's newsletter sending (its SES tenant).
|
|
165
|
+
"admin ses tenants block",
|
|
166
|
+
"admin ses tenants unblock",
|
|
156
167
|
// A noun leaf with its own action (not a group), so the default would
|
|
157
168
|
// advertise a STANDING per-viewer paid-enrichment toggle as a read. `enrichment`
|
|
158
169
|
// itself is not a MUTATING_VERBS entry because the other two `enrichment`
|
|
@@ -489,7 +500,8 @@ function toManifestEntry(command, path) {
|
|
|
489
500
|
// cap flag exists for either, so the flag heuristic alone would misreport
|
|
490
501
|
// them as free.
|
|
491
502
|
(path[0] === "copilot" &&
|
|
492
|
-
(leafVerb === "send" || leafVerb === "approve"))
|
|
503
|
+
(leafVerb === "send" || leafVerb === "approve")) ||
|
|
504
|
+
SPENDING_WITHOUT_APPROVAL_COMMANDS.has(commandName))),
|
|
493
505
|
mutates: operations.length ? operations.some((spec) => spec.mutates) : (!READ_ONLY_COMMANDS.has(commandName)
|
|
494
506
|
&& (MUTATING_COMMANDS.has(commandName)
|
|
495
507
|
|| MUTATING_VERBS.has(leafVerb)
|
package/dist/command-support.js
CHANGED
|
@@ -66,7 +66,7 @@ export function readCollabPath(value) {
|
|
|
66
66
|
* API refuses (focus_rank_unavailable). Unhide with the production rollout.
|
|
67
67
|
*/
|
|
68
68
|
export function leadsTodayRankOption() {
|
|
69
|
-
return new Option("--rank <rank>", "signal (default) or focus: warmth, then ICP fit, then decayed 90
|
|
69
|
+
return new Option("--rank <rank>", "signal (default) or focus: warmth, then ICP fit, then decayed inbound intent over 90 days (or --within-days), each lead with reasons, evidence ids and the teammates who know them. Only where Network Mining is enabled; elsewhere the API answers focus_rank_unavailable.").hideHelp();
|
|
70
70
|
}
|
|
71
71
|
// One cadence vocabulary across every scheduled surface (column refresh, table
|
|
72
72
|
// feed): `translateScheduleEvery` in apps/web resolves all four forms, so this
|
package/dist/commands/admin.js
CHANGED
|
@@ -343,5 +343,86 @@ export function registerAdminFamily(program, handleAsyncAction, binaryName) {
|
|
|
343
343
|
},
|
|
344
344
|
});
|
|
345
345
|
});
|
|
346
|
-
})))
|
|
346
|
+
})))
|
|
347
|
+
.addCommand(new Command("ses")
|
|
348
|
+
.description("The shared Amazon SES account's workspace ownership. Staff only.")
|
|
349
|
+
.addCommand(new Command("identities")
|
|
350
|
+
.description("Which workspace owns each SES identity (sending domain) on the shared account.")
|
|
351
|
+
.addCommand(new Command("list")
|
|
352
|
+
.description("List SES identity claims: owner organization, state (pending, verified, released, reserved), tenant and expiry.")
|
|
353
|
+
.option("--organization <uuid>", "Only this organization's identities.")
|
|
354
|
+
.option("--json", "Print a JSON envelope.")
|
|
355
|
+
.action(async (options) => {
|
|
356
|
+
const org = readOption(options.organization);
|
|
357
|
+
await handleAsyncAction("admin ses identities list", options, () => requestOxygen(`/api/cli/admin/ses/identities${org ? `?organization_id=${encodeURIComponent(org)}` : ""}`));
|
|
358
|
+
}))
|
|
359
|
+
.addCommand(new Command("adopt")
|
|
360
|
+
.description("Record an identity OXYGEN already runs on the account (its own newsletter domains, or one created before ownership was tracked) as owned by an organization. Bypasses the reserved-domain rule; refuses an identity another workspace holds unless --force.")
|
|
361
|
+
.argument("<identity>", "The SES domain identity, such as updates.oxygen-agent.com.")
|
|
362
|
+
.requiredOption("--organization <uuid>", "Control organization UUID that owns it.")
|
|
363
|
+
.option("--state <state>", "reserved (OXYGEN's own, default) or verified (a workspace's).")
|
|
364
|
+
.option("--force", "Take it from the workspace that holds it now.")
|
|
365
|
+
.option("--json", "Print a JSON envelope.")
|
|
366
|
+
.action(async (identity, options) => {
|
|
367
|
+
const state = readOption(options.state);
|
|
368
|
+
await handleAsyncAction("admin ses identities adopt", options, () => requestOxygen("/api/cli/admin/ses/identities", {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: { identity, organization_id: readOption(options.organization), ...(state ? { state } : {}), ...(options.force ? { force: true } : {}) },
|
|
371
|
+
}));
|
|
372
|
+
})))
|
|
373
|
+
.addCommand(new Command("tenants")
|
|
374
|
+
.description("Each workspace's SES tenant: newsletter health, proven volume, warm-up and staff controls.")
|
|
375
|
+
.addCommand(new Command("list")
|
|
376
|
+
.description("List every workspace's SES tenant: customer and SES status, 7-day sends, bounce and complaint rates, proven volume, warm-up day, allowance override and identities. Stopped workspaces first.")
|
|
377
|
+
.option("--json", "Print a JSON envelope.")
|
|
378
|
+
.action(async (options) => {
|
|
379
|
+
await handleAsyncAction("admin ses tenants list", options, () => requestOxygen("/api/cli/admin/ses/tenants"));
|
|
380
|
+
}))
|
|
381
|
+
.addCommand(sesTenantStatusCommand("pause", "Pause a workspace's newsletter sending until someone resumes it.", true, handleAsyncAction))
|
|
382
|
+
.addCommand(sesTenantStatusCommand("resume", "Resume a paused workspace's newsletter sending.", false, handleAsyncAction))
|
|
383
|
+
.addCommand(sesTenantStatusCommand("block", "Block a workspace's newsletter sending until staff unblock it.", true, handleAsyncAction))
|
|
384
|
+
.addCommand(sesTenantStatusCommand("unblock", "Lift a block; the workspace sends newsletters again.", false, handleAsyncAction))
|
|
385
|
+
.addCommand(new Command("set-allowance")
|
|
386
|
+
.description("Cap a workspace's daily newsletter allowance (emails per day; 0 stops sending). The override only lowers the plan's cap.")
|
|
387
|
+
.requiredOption("--organization <uuid>", "Control organization UUID.")
|
|
388
|
+
.option("--value <emails>", "Emails per day, 0 or more.")
|
|
389
|
+
.option("--clear", "Remove the override.")
|
|
390
|
+
.option("--json", "Print a JSON envelope.")
|
|
391
|
+
.action(async (options) => {
|
|
392
|
+
await handleAsyncAction("admin ses tenants set-allowance", options, () => {
|
|
393
|
+
const value = readAllowanceValue(options.value, options.clear === true);
|
|
394
|
+
const organization = readOption(options.organization) ?? "";
|
|
395
|
+
return requestOxygen(`/api/cli/admin/ses/tenants/${encodeURIComponent(organization)}`, { method: "POST", body: { action: "set-allowance", value } });
|
|
396
|
+
});
|
|
397
|
+
}))));
|
|
398
|
+
}
|
|
399
|
+
/** `--value` (a whole number, 0 allowed) or `--clear` (null), exactly one. */
|
|
400
|
+
function readAllowanceValue(value, clear) {
|
|
401
|
+
const trimmed = value?.trim();
|
|
402
|
+
if (clear === Boolean(trimmed)) {
|
|
403
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --value <emails> or --clear.", { exitCode: 2 });
|
|
404
|
+
}
|
|
405
|
+
if (clear)
|
|
406
|
+
return null;
|
|
407
|
+
const parsed = Number(trimmed);
|
|
408
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
409
|
+
throw new OxygenError("invalid_request", "--value must be a whole number of emails per day, 0 or more.", { details: { value }, exitCode: 2 });
|
|
410
|
+
}
|
|
411
|
+
return parsed;
|
|
412
|
+
}
|
|
413
|
+
function sesTenantStatusCommand(action, description, needsReason, handleAsyncAction) {
|
|
414
|
+
const command = new Command(action)
|
|
415
|
+
.description(description)
|
|
416
|
+
.requiredOption("--organization <uuid>", "Control organization UUID.");
|
|
417
|
+
if (needsReason)
|
|
418
|
+
command.requiredOption("--reason <text>", "Why; shown on the staff board.");
|
|
419
|
+
return command
|
|
420
|
+
.option("--json", "Print a JSON envelope.")
|
|
421
|
+
.action(async (options) => {
|
|
422
|
+
const organization = readOption(options.organization) ?? "";
|
|
423
|
+
await handleAsyncAction(`admin ses tenants ${action}`, options, () => requestOxygen(`/api/cli/admin/ses/tenants/${encodeURIComponent(organization)}`, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
body: needsReason ? { action, reason: readOption(options.reason) } : { action },
|
|
426
|
+
}));
|
|
427
|
+
});
|
|
347
428
|
}
|
package/dist/commands/billing.js
CHANGED
|
@@ -350,7 +350,7 @@ export function registerBillingFamily(program, handleAsyncAction, billingDocsUrl
|
|
|
350
350
|
.addCommand(new Command("topup")
|
|
351
351
|
.description("No amount: read-only price quote, no checkout. With --credits or a pack: create Stripe Checkout and possibly billing resources; no charge until payment. Read tax_basis and review tax and the final total in Checkout. Prices are per 100 credits (`usd_per_100_credits`); credits arrive after confirmed payment and never expire. Plan credits cost less. `usd_per_1k_credits` is a deprecated name for the same per-100 price, not a price per 1,000 credits. `default_credits` is the web form's initial custom selection, not a purchase or minimum.")
|
|
352
352
|
.argument("[pack]", "A pack id from the list shown when you run without an amount.")
|
|
353
|
-
.option("--credits <n>", "Credits to buy (800-
|
|
353
|
+
.option("--credits <n>", "Credits to buy (800-769,200 in 100-credit increments).")
|
|
354
354
|
.option("--json", "Print a JSON envelope.")
|
|
355
355
|
.action(async (pack, options) => {
|
|
356
356
|
const credits = readOption(options.credits);
|
package/dist/commands/copilot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emitCliFailure, emitSuccess } from "../cli-output.js";
|
|
2
2
|
import { readJsonObjectOption, readNonNegativeInt, readPositiveInt, readRecordString } from "../cli-values.js";
|
|
3
|
-
import { collectRepeatable, readPositiveNumber } from "../command-support.js";
|
|
3
|
+
import { collectRepeatable, isUuid, readPositiveNumber } from "../command-support.js";
|
|
4
4
|
import { addContractFlags, readContractFlags } from "../contract-flags.js";
|
|
5
5
|
import { requestOxygen } from "../http-client.js";
|
|
6
6
|
import { waitForCliRun } from "../run-wait.js";
|
|
@@ -483,7 +483,8 @@ export function registerCopilotFamily(program, handleAsyncAction) {
|
|
|
483
483
|
.option("--no-wait", "Post the turn and return immediately without streaming the reply.")
|
|
484
484
|
.option("--context <json>", "Optional JSON object of surface context to attach to the turn.")
|
|
485
485
|
.option("--attach <path>", "Attach a file to this message (repeatable). Spreadsheets become tables the copilot can import and enrich; text documents ground the conversation. Uploaded straight to object storage, so large files are fine.", collectRepeatable, [])
|
|
486
|
-
.option("--
|
|
486
|
+
.option("--attachment-id <id>", "Include a ready file already uploaded to this session by you (repeatable). Use the id returned by copilot attach or attachments; may be combined with --attach local paths.", collectRepeatable, [])
|
|
487
|
+
.option("--interrupt", "In a Copilot chat, stop a still-working turn and send this message as the next turn instead of being refused. Not with attachments or in an Oxygent chat.")
|
|
487
488
|
.option("--timeout-seconds <n>", "Maximum time to wait for the turn to finish. Defaults to 300.")
|
|
488
489
|
.option("--interval-seconds <n>", "Polling interval while streaming. Defaults to 2.")
|
|
489
490
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -491,17 +492,23 @@ export function registerCopilotFamily(program, handleAsyncAction) {
|
|
|
491
492
|
await handleAsyncAction("copilot send", options, async () => {
|
|
492
493
|
const message = messageParts.join(" ");
|
|
493
494
|
const context = readJsonObjectOption(options.context);
|
|
494
|
-
const attachmentIds =
|
|
495
|
+
const attachmentIds = new Set();
|
|
496
|
+
for (const id of options.attachmentId ?? []) {
|
|
497
|
+
if (!isUuid(id)) {
|
|
498
|
+
throw new OxygenError("invalid_option", "--attachment-id must be a UUID returned by copilot attach or attachments.", { exitCode: 2 });
|
|
499
|
+
}
|
|
500
|
+
attachmentIds.add(id.toLowerCase());
|
|
501
|
+
}
|
|
495
502
|
for (const path of options.attach ?? []) {
|
|
496
503
|
const attached = await uploadCopilotAttachment(sessionId, path);
|
|
497
|
-
attachmentIds.
|
|
504
|
+
attachmentIds.add(attached.id);
|
|
498
505
|
}
|
|
499
506
|
const posted = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/turns`, {
|
|
500
507
|
method: "POST",
|
|
501
508
|
body: {
|
|
502
509
|
message,
|
|
503
510
|
...(context ? { context } : {}),
|
|
504
|
-
...(attachmentIds.
|
|
511
|
+
...(attachmentIds.size > 0 ? { attachment_ids: [...attachmentIds] } : {}),
|
|
505
512
|
...(options.interrupt ? { interrupt: true } : {}),
|
|
506
513
|
},
|
|
507
514
|
});
|
|
@@ -530,7 +537,7 @@ export function registerCopilotFamily(program, handleAsyncAction) {
|
|
|
530
537
|
});
|
|
531
538
|
}))
|
|
532
539
|
.addCommand(new Command("attach")
|
|
533
|
-
.description("Attach a file to a Workspace Copilot session without sending a message yet. The file uploads straight to object storage and is described back to you (a spreadsheet's columns and row count)
|
|
540
|
+
.description("Attach a file to a Workspace Copilot session without sending a message yet. The file uploads straight to object storage and is described back to you (a spreadsheet's columns and row count). Include it in a turn with `copilot send --attachment-id <id>`; unsent files are not included automatically.")
|
|
534
541
|
.argument("<sessionId>", "Copilot session id.")
|
|
535
542
|
.argument("<path>", "Path to the file to attach.")
|
|
536
543
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -540,11 +547,26 @@ export function registerCopilotFamily(program, handleAsyncAction) {
|
|
|
540
547
|
})));
|
|
541
548
|
}))
|
|
542
549
|
.addCommand(new Command("attachments")
|
|
543
|
-
.description("List
|
|
550
|
+
.description("List sent chat files and your own unsent files. Download originals with copilot attachment-download.")
|
|
544
551
|
.argument("<sessionId>", "Copilot session id.")
|
|
545
552
|
.option("--json", "Print a JSON envelope.")
|
|
546
553
|
.action(async (sessionId, options) => {
|
|
547
554
|
await handleAsyncAction("copilot attachments", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/attachments`));
|
|
555
|
+
}))
|
|
556
|
+
.addCommand(new Command("attachment-download")
|
|
557
|
+
.description("Download an original chat file. Current chat access is required; unsent files are private to their uploader.")
|
|
558
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
559
|
+
.argument("<attachmentId>", "Attachment id from copilot attachments.")
|
|
560
|
+
.requiredOption("--output <path>", "New local file path; existing files are never overwritten.")
|
|
561
|
+
.option("--json", "Print a JSON envelope.")
|
|
562
|
+
.action(async (sessionId, attachmentId, options) => {
|
|
563
|
+
await handleAsyncAction("copilot attachment-download", options, () => {
|
|
564
|
+
if (!isUuid(sessionId) || !isUuid(attachmentId))
|
|
565
|
+
throw new OxygenError("invalid_option", "sessionId and attachmentId must be UUIDs.", { exitCode: 2 });
|
|
566
|
+
return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(attachmentId)}`, {
|
|
567
|
+
binaryOutputPath: resolve(options.output),
|
|
568
|
+
});
|
|
569
|
+
});
|
|
548
570
|
}))
|
|
549
571
|
.addCommand(new Command("approve")
|
|
550
572
|
.description("Approve (or, with --reject, reject) an action a Workspace Copilot session is waiting on; approving re-queues the paused turn.")
|
package/dist/commands/domains.js
CHANGED
|
@@ -393,6 +393,13 @@ function sendingDomainsCommand(handleAsyncAction) {
|
|
|
393
393
|
.option("--json", "Print a JSON envelope.")
|
|
394
394
|
.action(async (id, options) => {
|
|
395
395
|
await handleAsyncAction("domains sending resume", options, () => runSendingDomainPause(id, false, undefined));
|
|
396
|
+
}))
|
|
397
|
+
.addCommand(new Command("release")
|
|
398
|
+
.description("Remove a sending domain and give its domain back so it can be added again. Refused while any of its bulk emails is waiting to send.")
|
|
399
|
+
.argument("<domain>", "Sending domain id or name.")
|
|
400
|
+
.option("--json", "Print a JSON envelope.")
|
|
401
|
+
.action(async (id, options) => {
|
|
402
|
+
await handleAsyncAction("domains sending release", options, () => requestOxygen(`/api/cli/sequencer/sending-domains/${encodeURIComponent(id)}/release`, { method: "POST" }));
|
|
396
403
|
}))
|
|
397
404
|
.addCommand(settingsFlags(new Command("update")
|
|
398
405
|
.description("Change a sending domain's daily ceiling, allowed From addresses or reply-to mailbox.")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { requestOxygen } from "../http-client.js";
|
|
2
|
+
import { readOption } from "../util.js";
|
|
3
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { basename, extname, resolve } from "node:path";
|
|
7
|
+
// Newsletter image hosting: `/api/cli/email-assets`. The server re-encodes every
|
|
8
|
+
// image (metadata stripped, at most 1500 px wide, JPEG/PNG, animated GIF kept,
|
|
9
|
+
// never SVG) and returns a permanent public URL to place in an email.
|
|
10
|
+
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const MIME_BY_EXTENSION = {
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".webp": "image/webp",
|
|
17
|
+
".avif": "image/avif",
|
|
18
|
+
".svg": "image/svg+xml",
|
|
19
|
+
".heic": "image/heic",
|
|
20
|
+
".heif": "image/heif",
|
|
21
|
+
".tif": "image/tiff",
|
|
22
|
+
".tiff": "image/tiff",
|
|
23
|
+
};
|
|
24
|
+
function isUrl(source) {
|
|
25
|
+
return /^https?:\/\//i.test(source.trim());
|
|
26
|
+
}
|
|
27
|
+
async function uploadFromFile(path) {
|
|
28
|
+
const absolute = resolve(path);
|
|
29
|
+
let size;
|
|
30
|
+
try {
|
|
31
|
+
size = (await stat(absolute)).size;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new OxygenError("file_not_found", `No file at ${path}.`, { details: { field: "source" }, exitCode: 2 });
|
|
35
|
+
}
|
|
36
|
+
if (size > MAX_UPLOAD_BYTES) {
|
|
37
|
+
throw new OxygenError("email_asset_too_large", "The image is larger than 10 MB. Export a smaller image and try again.", {
|
|
38
|
+
details: { max_bytes: MAX_UPLOAD_BYTES, byte_length: size },
|
|
39
|
+
exitCode: 1,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const bytes = await readFile(absolute);
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
const type = MIME_BY_EXTENSION[extname(absolute).toLowerCase()] ?? "application/octet-stream";
|
|
45
|
+
form.append("file", new File([bytes], basename(absolute), { type }));
|
|
46
|
+
return requestOxygen("/api/cli/email-assets", { method: "POST", formData: form });
|
|
47
|
+
}
|
|
48
|
+
export function registerEmailAssetsFamily(program, handleAsyncAction) {
|
|
49
|
+
program.addCommand(new Command("email-assets")
|
|
50
|
+
.description("Host images for newsletters and emails: upload once, get a permanent public URL. Images are re-encoded (EXIF stripped, at most 1500 px wide, JPEG or PNG; animated GIFs up to 2 MB kept). 0 credits.")
|
|
51
|
+
.addCommand(new Command("upload")
|
|
52
|
+
.description("Upload a local image file or fetch a public https image URL (max 10 MB; JPEG, PNG, GIF, WebP, AVIF, SVG). The same image twice returns the first upload.")
|
|
53
|
+
.argument("<file|url>", "Path to an image file, or an https:// image URL.")
|
|
54
|
+
.option("--json", "Print a JSON envelope.")
|
|
55
|
+
.action(async (source, options) => {
|
|
56
|
+
await handleAsyncAction("email-assets upload", options, () => (isUrl(source)
|
|
57
|
+
? requestOxygen("/api/cli/email-assets", { method: "POST", body: { source_url: source.trim() } })
|
|
58
|
+
: uploadFromFile(source)));
|
|
59
|
+
}))
|
|
60
|
+
.addCommand(new Command("list")
|
|
61
|
+
.description("List the workspace's hosted email images, newest first.")
|
|
62
|
+
.option("--limit <n>", "Images per page, 1-200 (default 50).")
|
|
63
|
+
.option("--before <timestamp>", "Page cursor: the previous page's next_before.")
|
|
64
|
+
.option("--json", "Print a JSON envelope.")
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
await handleAsyncAction("email-assets list", options, () => {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
const limit = readOption(options.limit);
|
|
69
|
+
const before = readOption(options.before);
|
|
70
|
+
if (limit)
|
|
71
|
+
params.set("limit", limit);
|
|
72
|
+
if (before)
|
|
73
|
+
params.set("before", before);
|
|
74
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
75
|
+
return requestOxygen(`/api/cli/email-assets${query}`);
|
|
76
|
+
});
|
|
77
|
+
})));
|
|
78
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { requestOxygen } from "../http-client.js";
|
|
2
|
+
import { contentBody, readRevisionFlag, readSequence } from "../sequences-content-commands.js";
|
|
3
|
+
import { readOption } from "../util.js";
|
|
4
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
5
|
+
import { sequenceContentStep } from "@oxygen/shared/sequences";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
async function saveTemplate(options) {
|
|
8
|
+
const fromDocument = readOption(options.fromDocument);
|
|
9
|
+
const fromStarter = readOption(options.fromStarter);
|
|
10
|
+
const sequence = readOption(options.sequence);
|
|
11
|
+
const fromFile = Boolean(options.htmlFile || options.jsonFile);
|
|
12
|
+
const sources = [fromDocument, fromStarter, sequence, fromFile].filter(Boolean).length;
|
|
13
|
+
if (sources !== 1) {
|
|
14
|
+
throw new OxygenError(sources > 1 ? "conflicting_flags" : "invalid_request", "Pass exactly one of --from-document, --from-starter, --sequence with --step, or --html-file/--json-file.", { exitCode: 2 });
|
|
15
|
+
}
|
|
16
|
+
const name = readOption(options.name);
|
|
17
|
+
const revision = readRevisionFlag(options.revision, "--revision");
|
|
18
|
+
if (revision !== undefined && !fromDocument)
|
|
19
|
+
throw new OxygenError("invalid_request", "--revision applies to --from-document.", { exitCode: 2 });
|
|
20
|
+
if (fromStarter) {
|
|
21
|
+
return requestOxygen("/api/cli/email-documents", { method: "POST", body: { role: "template", starter_id: fromStarter, ...(name ? { name } : {}) } });
|
|
22
|
+
}
|
|
23
|
+
if (fromFile) {
|
|
24
|
+
return requestOxygen("/api/cli/email-documents", { method: "POST", body: { role: "template", ...contentBody(options) } });
|
|
25
|
+
}
|
|
26
|
+
let sourceId = fromDocument;
|
|
27
|
+
let sourceRevision = revision;
|
|
28
|
+
if (sequence) {
|
|
29
|
+
const stepId = readOption(options.step);
|
|
30
|
+
if (!stepId)
|
|
31
|
+
throw new OxygenError("invalid_request", "--step is required with --sequence.", { exitCode: 2 });
|
|
32
|
+
const read = await readSequence(sequence);
|
|
33
|
+
const { bodyDocument } = sequenceContentStep(read.sequence.definition, stepId);
|
|
34
|
+
if (!bodyDocument) {
|
|
35
|
+
throw new OxygenError("email_document_not_found", "That step has no designed email to save; set one with `oxygen sequences content set` first.", {
|
|
36
|
+
details: { sequence_id: read.sequence.id, step_id: stepId }, exitCode: 4,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
sourceId = bodyDocument.id;
|
|
40
|
+
sourceRevision = bodyDocument.revision;
|
|
41
|
+
}
|
|
42
|
+
return requestOxygen(`/api/cli/email-documents/${encodeURIComponent(sourceId)}`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: { action: "duplicate", role: "template", ...(sourceRevision !== undefined ? { revision: sourceRevision } : {}), ...(name ? { name } : {}) },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function registerEmailTemplatesFamily(program, handleAsyncAction) {
|
|
48
|
+
program.addCommand(new Command("email-templates")
|
|
49
|
+
.description("The newsletter template library: reusable email designs saved from a step, a file or a built-in starter. Start a step from one with `oxygen sequences content set --from-document <id>` (or `--from-starter <id>`). 0 credits.")
|
|
50
|
+
.addCommand(new Command("list")
|
|
51
|
+
.description("List the workspace's email templates (name, updated_at, preview text, HTML size) and the built-in read-only starters.")
|
|
52
|
+
.option("--include-archived", "Include deleted templates.")
|
|
53
|
+
.option("--json", "Print a JSON envelope.")
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
await handleAsyncAction("email-templates list", options, () => requestOxygen(`/api/cli/email-documents?role=template${options.includeArchived ? "&include_archived=true" : ""}`));
|
|
56
|
+
}))
|
|
57
|
+
.addCommand(new Command("save")
|
|
58
|
+
.description("Save a template: from a step's current email (--sequence --step, save-as), an email document or template (--from-document), a built-in starter (--from-starter) or an HTML/JSON file. Copies the current revision; later edits to the source do not change it.")
|
|
59
|
+
.option("--name <name>", "Template name.")
|
|
60
|
+
.option("--sequence <id>", "With --step: save this sequence step's email as a template.")
|
|
61
|
+
.option("--step <id>", "The email_send step id (with --sequence).")
|
|
62
|
+
.option("--from-document <id>", "Copy this email document or template.")
|
|
63
|
+
.option("--revision <n>", "With --from-document: copy this revision (default its current one).")
|
|
64
|
+
.option("--from-starter <id>", "Copy a built-in starter: simple-announcement, product-update, weekly-digest.")
|
|
65
|
+
.option("--html-file <path>", "The email HTML.")
|
|
66
|
+
.option("--json-file <path>", "A JSON object { html, text?, preview_text?, editor_json? }, e.g. the editor's export.")
|
|
67
|
+
.option("--text-file <path>", "The plain-text part. Default: derived from the HTML.")
|
|
68
|
+
.option("--preview-text <text>", "The inbox preview line shown after the subject.")
|
|
69
|
+
.option("--json", "Print a JSON envelope.")
|
|
70
|
+
.action(async (options) => {
|
|
71
|
+
await handleAsyncAction("email-templates save", options, () => saveTemplate(options));
|
|
72
|
+
}))
|
|
73
|
+
.addCommand(new Command("rename")
|
|
74
|
+
.description("Rename a template (no new revision).")
|
|
75
|
+
.argument("<template-id>", "Template id.")
|
|
76
|
+
.requiredOption("--name <name>", "New name.")
|
|
77
|
+
.option("--json", "Print a JSON envelope.")
|
|
78
|
+
.action(async (templateId, options) => {
|
|
79
|
+
await handleAsyncAction("email-templates rename", options, () => requestOxygen(`/api/cli/email-documents/${encodeURIComponent(templateId)}`, { method: "PATCH", body: { name: options.name } }));
|
|
80
|
+
}))
|
|
81
|
+
.addCommand(new Command("duplicate")
|
|
82
|
+
.description("Copy a template into a new template (default name: \"<name> (copy)\").")
|
|
83
|
+
.argument("<template-id>", "Template id.")
|
|
84
|
+
.option("--name <name>", "Name of the copy.")
|
|
85
|
+
.option("--json", "Print a JSON envelope.")
|
|
86
|
+
.action(async (templateId, options) => {
|
|
87
|
+
await handleAsyncAction("email-templates duplicate", options, () => {
|
|
88
|
+
const name = readOption(options.name);
|
|
89
|
+
return requestOxygen(`/api/cli/email-documents/${encodeURIComponent(templateId)}`, {
|
|
90
|
+
method: "POST", body: { action: "duplicate", role: "template", ...(name ? { name } : {}) },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}))
|
|
94
|
+
.addCommand(new Command("delete")
|
|
95
|
+
.description("Delete a template from the library. Sequences already using a copy keep it; a step's own email cannot be deleted here.")
|
|
96
|
+
.argument("<template-id>", "Template id.")
|
|
97
|
+
.option("--json", "Print a JSON envelope.")
|
|
98
|
+
.action(async (templateId, options) => {
|
|
99
|
+
await handleAsyncAction("email-templates delete", options, () => requestOxygen(`/api/cli/email-documents/${encodeURIComponent(templateId)}`, { method: "DELETE" }));
|
|
100
|
+
})));
|
|
101
|
+
}
|