@oxygen-agent/cli 1.1014.1600 → 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 +18 -3
- 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/custom-integrations.js +13 -0
- 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/inbox.js +1 -1
- package/dist/commands/integrations.js +5 -1
- package/dist/commands/mailboxes.js +26 -6
- package/dist/commands/managed-inboxes.js +2 -2
- package/dist/commands/senders.js +4 -4
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/credential-storage/store.d.ts +10 -0
- package/dist/credential-storage/store.js +282 -0
- package/dist/credentials.d.ts +1 -3
- package/dist/credentials.js +3 -275
- package/dist/mailbox-fleet-output.js +11 -2
- 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/dist/workspace-placement-commands.js +3 -2
- 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 +236 -18
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
- 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/customer-operations.js +2 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
- 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-classification-scope.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -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/native-chat.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/native-chat.js +12 -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/onboarding-research.d.ts +9 -2
- package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
- 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/recipes.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +13 -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 +30 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
- 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/tool-call-arguments.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
- 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/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
- package/package.json +2 -2
|
@@ -553,15 +553,23 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
553
553
|
}))
|
|
554
554
|
.addCommand(new Command("health")
|
|
555
555
|
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations — plus OXYGEN's OWN evidence for the last 7 days, which needs no provider connected: the bounce notifications each mailbox received classified as provider rejection / dead address / mailbox full / delay (a Google receiving-server policy rejection shows up as provider_rejected), distinct recipients with a hard bounce in the window (legacy facts retain their first observation time), sequence sends OXYGEN logged (source=sequence only), warm-up day, health score and stop cause, each mailbox's configured daily cap, and the same rollup per sending domain ranked worst-first. Notifications whose body was never stored count as signals.dsn7d.unclassified, which means OXYGEN could not look — not that nothing was wrong. Read health_coverage to see how many mailboxes an external provider has actually scored. Start from the fleet read instead of scanning the mailbox array: warmup.byCause (why warm-up stopped, counted once for the whole pool). Per-mailbox warmup.state and fleet warmup.byState are stored rail telemetry; use `oxygen mailboxes list --json` warmupTruth for enrollment state and next actions. The rolling window is signals.window (7d). Delivery notifications include all send sources, including warm-up and manual mail; signals.sendSource=sequence restricts only coldSends7d. Identified warm-up rejections are reported separately, and other notifications have no verified send-source attribution. Rejections alone do not establish that prospect mail was rejected or that a whole domain is blocked. A zero means no such evidence was recorded in the stated window, not that the fleet is un-blocklisted everywhere. Pure read — 0 credits; never pauses a mailbox or changes a cap.")
|
|
556
|
-
.addOption(new Option("--view <view>", "
|
|
556
|
+
.addOption(new Option("--view <view>", "Start with --view fleet for totals; summary: complete compact roster (default); full: per-inbox evidence.").choices(["summary", "fleet", "full"]))
|
|
557
|
+
.option("--mailboxes <list>", "Inspect 1–100 mailbox ids or addresses, comma-separated (defaults to full evidence). Fleet totals remain unfiltered.")
|
|
557
558
|
.option("--json", "Print a JSON envelope.")
|
|
558
559
|
.action(async (options) => {
|
|
559
|
-
|
|
560
|
+
const query = new URLSearchParams({ view: options.view ?? (options.mailboxes !== undefined ? "full" : "summary") });
|
|
561
|
+
if (options.mailboxes !== undefined)
|
|
562
|
+
query.set("mailboxes", options.mailboxes);
|
|
563
|
+
await handleReadActionWithLens("mailboxes health", options, () => requestOxygen(`/api/cli/mailboxes/health?${query}`), formatMailboxFleetHealth);
|
|
560
564
|
}))
|
|
561
565
|
.addCommand(new Command("compatibility")
|
|
562
|
-
.description("Read-only compatibility report per mailbox — select with --mailboxes <address,…> (omit for the whole pool; filter here, not in a script): generic origin class, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, OXYGEN Warm-up path, OXYGEN deliverability monitoring path, and exact next actions. Read automatic_repair for native send and warmup_automatic_repair for an existing warm-up seat: mode=automatic means OXYGEN owns the next bounded retry. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the current public product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts
|
|
566
|
+
.description("Read-only compatibility report per mailbox — select with --mailboxes <address,…> (omit for the whole pool; filter here, not in a script): generic origin class, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, OXYGEN Warm-up path, OXYGEN deliverability monitoring path, and exact next actions. Read automatic_repair for native send and warmup_automatic_repair for an existing warm-up seat: mode=automatic means OXYGEN owns the next bounded retry. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the current public product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts credentials or writes provider state. The full report may read InboxKit source-warmup status; --remedy oauth uses stored authorization only with no provider calls and leaves warm-up and deliverability unchecked.")
|
|
563
567
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
564
568
|
.option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and public provider catalogs; do not read or return workspace mailbox rows.")
|
|
569
|
+
.option("--vendor <origin>", "Filter the canonical mailbox origin, for example inboxkit, zapmail or native.")
|
|
570
|
+
.option("--provider <provider>", "Filter google or microsoft.")
|
|
571
|
+
.option("--remedy <remedy>", "Filter a native-send remedy code; oauth reports stored authorization only without provider calls.")
|
|
572
|
+
.option("--summary-only", "Return counts without individual mailbox rows.")
|
|
565
573
|
.option("--json", "Print a JSON envelope.")
|
|
566
574
|
.action(async (options) => {
|
|
567
575
|
await handleAsyncAction("mailboxes compatibility", options, () => {
|
|
@@ -576,6 +584,13 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
576
584
|
if (mailboxes.length > 0) {
|
|
577
585
|
params.set("mailboxes", mailboxes.join(","));
|
|
578
586
|
}
|
|
587
|
+
for (const key of ["vendor", "provider", "remedy"]) {
|
|
588
|
+
const value = readOption(options[key]);
|
|
589
|
+
if (value)
|
|
590
|
+
params.set(key, value);
|
|
591
|
+
}
|
|
592
|
+
if (options.summaryOnly)
|
|
593
|
+
params.set("summary_only", "true");
|
|
579
594
|
const suffix = params.toString();
|
|
580
595
|
return requestOxygen(`/api/cli/mailboxes/compatibility${suffix ? `?${suffix}` : ""}`);
|
|
581
596
|
});
|
|
@@ -781,12 +796,13 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
781
796
|
});
|
|
782
797
|
})))
|
|
783
798
|
.addCommand(new Command("update")
|
|
784
|
-
.description("Update a sending mailbox's rich-content profile: the From display name and/or the HTML signature appended to its sends. An email that already contains the signature gets no second copy. Pass an empty string ('') to clear a field. Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
799
|
+
.description("Update a sending mailbox's rich-content profile: the From display name and/or the HTML signature appended to its sends. An email that already contains the signature gets no second copy. Pass an empty string ('') to clear a field. Also sets its bounce protection and which inbound mail AI reply classification triages (--classify). Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
785
800
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
786
801
|
.option("--display-name <name>", "From display name (e.g. \"Ada from Acme\"). Pass '' to clear it.")
|
|
787
802
|
.option("--signature-html <html>", "HTML signature appended to every send from this mailbox. Pass '' to clear it.")
|
|
788
803
|
.option("--signature-file <path>", "Read the HTML signature from a file instead of --signature-html.")
|
|
789
804
|
.option("--bounce-protection <json>", "Mailbox bounce policy (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this mailbox. Rates are fractions; sequence protection is separate.")
|
|
805
|
+
.option("--classify <scope>", "Which inbound conversations AI reply classification triages (each one uses credits): replies (Sequence threads and threads you wrote in; other mail, such as newsletters and notifications, stays in Unibox unclassified at no cost) or all (every inbound conversation). Default: replies for a Google mailbox you added yourself, all for every other mailbox. Applies to new mail; `mailboxes get` shows the current scope.")
|
|
790
806
|
.option("--json", "Print a JSON envelope.")
|
|
791
807
|
.action(async (mailbox, options) => {
|
|
792
808
|
await handleAsyncAction("mailboxes update", options, () => {
|
|
@@ -795,6 +811,8 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
795
811
|
const patch = {};
|
|
796
812
|
if (options.bounceProtection !== undefined)
|
|
797
813
|
patch.bounce_protection = JSON.parse(options.bounceProtection);
|
|
814
|
+
if (options.classify !== undefined)
|
|
815
|
+
patch.classification_scope = options.classify;
|
|
798
816
|
if (options.displayName !== undefined)
|
|
799
817
|
patch.display_name = options.displayName;
|
|
800
818
|
if (options.signatureHtml !== undefined) {
|
|
@@ -804,7 +822,7 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
804
822
|
patch.signature_html = readFileSync(resolve(options.signatureFile), "utf8");
|
|
805
823
|
}
|
|
806
824
|
if (Object.keys(patch).length === 0) {
|
|
807
|
-
throw new Error("Provide --display-name
|
|
825
|
+
throw new Error("Provide --display-name, --signature-html (or --signature-file), --bounce-protection or --classify.");
|
|
808
826
|
}
|
|
809
827
|
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`, {
|
|
810
828
|
method: "PATCH",
|
|
@@ -836,8 +854,9 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
836
854
|
.option("--no-canary", "inboxkit only: fan out to every eligible mailbox on a domain that has never connected one, instead of firing a single canary first.")
|
|
837
855
|
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
|
|
838
856
|
.option("--status <id>", "Poll a previously started Zapmail, InboxKit, or OXYGEN direct-consent run.")
|
|
839
|
-
.option("--dry-run", "Preview the requested mailbox/domain scope and external writes without changing vendor or ledger state.
|
|
857
|
+
.option("--dry-run", "Preview the requested mailbox/domain scope and external writes without changing vendor or ledger state. InboxKit enumerates consent candidates, skipped reasons, domain approvals and exact external writes; approve its fresh plan hash. This is the default for starts.")
|
|
840
858
|
.option("--approved", "Start the reviewed authorization run. Composio creates an individual consent link for each owner, who must open it and approve access with Google or Microsoft. Native vendor=oxygen records expiring browser steps for the selected owner or tenant administrator. Starting a run does not grant mail access.")
|
|
859
|
+
.option("--plan-hash <hash>", "InboxKit: exact fresh eligibility/approval preview hash; required with --approved.")
|
|
841
860
|
.option("--json", "Print a JSON envelope.")
|
|
842
861
|
.action(async (options) => {
|
|
843
862
|
await handleAsyncAction("mailboxes connect-oauth", options, () => {
|
|
@@ -867,6 +886,7 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
|
867
886
|
method: "POST",
|
|
868
887
|
body: {
|
|
869
888
|
provider,
|
|
889
|
+
...(readOption(options.planHash) ? { plan_hash: readOption(options.planHash) } : {}),
|
|
870
890
|
...(options.googleSmtp === true ? { google_smtp: true } : {}),
|
|
871
891
|
// Every key stays absent unless it was asked for, so a run
|
|
872
892
|
// without the new flags is the request this command has always
|
|
@@ -126,8 +126,8 @@ export function registerManagedInboxesFamily(program, handleAsyncAction) {
|
|
|
126
126
|
.addOption(new Option("--preview", "Explicit read-only quote: connects nothing, buys nothing and charges nothing. Cannot be combined with purchase or DNS cleanup approval.").conflicts(["approved", "dnsCleanupApproved"]))
|
|
127
127
|
.option("--existing-domain", "Use an existing Cloudflare domain from oxygen domains list; never register or transfer it.")
|
|
128
128
|
.option("--max-per-domain <n>", "Save a stricter mailbox limit with the approved order, including existing addresses (e.g. 2). Preview does not save it.")
|
|
129
|
-
.option("--dns-cleanup-records <ids>", "
|
|
130
|
-
.addOption(new Option("--dns-cleanup-approved", "Delete only the reviewed email records; requires --dns-cleanup-records and --dns-cleanup-hash. Request a fresh purchase quote afterward.").conflicts("approved"))
|
|
129
|
+
.option("--dns-cleanup-records <ids>", "Optional standalone cleanup: preview deletion of these comma-separated email DNS record IDs; buys nothing. An approved existing-domain order already removes the records its preview lists.")
|
|
130
|
+
.addOption(new Option("--dns-cleanup-approved", "Delete only the reviewed email records without buying; requires --dns-cleanup-records and --dns-cleanup-hash. Request a fresh purchase quote afterward.").conflicts("approved"))
|
|
131
131
|
.option("--dns-cleanup-hash <hash>", "The plan_hash from the exact DNS cleanup preview, required to delete records.")
|
|
132
132
|
.option("--years <n>", "Years to register a new domain for (1-10). Omit for existing domains.")
|
|
133
133
|
.option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
|
package/dist/commands/senders.js
CHANGED
|
@@ -203,15 +203,15 @@ export function registerSendersFamily(program, handleAsyncAction) {
|
|
|
203
203
|
.description("Show current limits, overrides, working hours (timezone, days, start, end), defaults, hard maximums, and the caution levels above which LinkedIn restriction risk rises, for an account. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
204
204
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
205
205
|
.option("--json", "Print a JSON envelope.")
|
|
206
|
-
.action(async (id, options) => {
|
|
207
|
-
await handleAsyncAction("senders limits get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
206
|
+
.action(async (id, options, command) => {
|
|
207
|
+
await handleAsyncAction("senders limits get", { ...options, json: command.optsWithGlobals().json === true }, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
208
208
|
}))
|
|
209
209
|
.addCommand(addContractFlags(new Command(SENDER_LIMITS_SET_OPERATION.cli.path.at(-1) ?? "set")
|
|
210
210
|
.description(SENDER_LIMITS_SET_OPERATION.cli.summary)
|
|
211
211
|
.argument("<id>", SENDER_LIMITS_SET_OPERATION.cli.argument?.description), SENDER_LIMITS_SET_OPERATION.contract, SENDER_LIMITS_FLAG_OVERLAY)
|
|
212
212
|
.option("--json", "Print a JSON envelope.")
|
|
213
|
-
.action(async (id, options) => {
|
|
214
|
-
await handleAsyncAction("senders limits set", options, () => {
|
|
213
|
+
.action(async (id, options, command) => {
|
|
214
|
+
await handleAsyncAction("senders limits set", { ...options, json: command.optsWithGlobals().json === true }, () => {
|
|
215
215
|
const body = buildSenderLimitsBody(options);
|
|
216
216
|
return requestOxygen(operationPath(SENDER_LIMITS_SET_OPERATION, { id }), {
|
|
217
217
|
method: SENDER_LIMITS_SET_OPERATION.method,
|
|
@@ -3,7 +3,12 @@ import { parseJsonObject, readNonNegativeInt, readPositiveInt } from "../cli-val
|
|
|
3
3
|
import { ansi, collectRepeatable, handleReadActionWithLens, parseJsonValue, readCreditCeilingOrZero, readCsvOption, readJsonFileValue, readPositiveNumber, readPublishingPostText, renderTextTable, splitCommaList } from "../command-support.js";
|
|
4
4
|
import { addContractFlags, readContractFlags } from "../contract-flags.js";
|
|
5
5
|
import { requestOxygen } from "../http-client.js";
|
|
6
|
+
import { sequencesBrandKitCommand } from "../sequences-brand-kit-command.js";
|
|
7
|
+
import { sequencesContentCommand } from "../sequences-content-commands.js";
|
|
8
|
+
import { formatNewsletterStatsLines, readNewsletterIntervalFlag, sequencesRecipientsCommand } from "../sequences-newsletter-commands.js";
|
|
9
|
+
import { sequencesTestSendCommand } from "../sequences-test-send-command.js";
|
|
6
10
|
import { networkMiningHostServed } from "../network-mining-host.js";
|
|
11
|
+
import { parseStartAtFlag } from "../sequence-start-at.js";
|
|
7
12
|
import { parseWorkingDaysOption } from "../sender-limits-flags.js";
|
|
8
13
|
import { isRecord, readOption } from "../util.js";
|
|
9
14
|
import { OxygenError, success } from "@oxygen/shared/cli-result";
|
|
@@ -576,6 +581,10 @@ function formatSequenceChannelFunnels(funnels, replies) {
|
|
|
576
581
|
const channels = ["linkedin", "email", "whatsapp"].filter((channel) => isRecord(byChannel[channel]));
|
|
577
582
|
if (channels.length === 0)
|
|
578
583
|
return [];
|
|
584
|
+
// OXYGEN offers no open tracking, so on such a deployment every channel's
|
|
585
|
+
// opens are not_supported and the column would read n/a on every row (Tim,
|
|
586
|
+
// 2026-10-22: hide it). It stays while any channel can report opens.
|
|
587
|
+
const showOpened = channels.some((channel) => stringValue(recordValue(recordValue(recordValue(byChannel[channel]).coverage).opened).state) !== "not_supported");
|
|
579
588
|
const rows = channels.map((channel) => {
|
|
580
589
|
const funnel = recordValue(byChannel[channel]);
|
|
581
590
|
const invites = recordValue(funnel.invites);
|
|
@@ -585,13 +594,13 @@ function formatSequenceChannelFunnels(funnels, replies) {
|
|
|
585
594
|
numberText(funnel.sends),
|
|
586
595
|
numericValue(invites.sent) === null ? "n/a" : `${numberText(invites.accepted)}/${numberText(invites.sent)}`,
|
|
587
596
|
funnelMetricText(funnel, "delivered"),
|
|
588
|
-
funnelMetricText(funnel, "opened"),
|
|
597
|
+
...(showOpened ? [funnelMetricText(funnel, "opened")] : []),
|
|
589
598
|
funnelMetricText(funnel, "replied"),
|
|
590
599
|
funnelRateText(funnel, "replyRate", "replied"),
|
|
591
600
|
funnelMetricText(funnel, "positiveReplied"),
|
|
592
601
|
];
|
|
593
602
|
});
|
|
594
|
-
const lines = ["", ...renderTextTable(["CHANNEL", "PEOPLE", "SENDS", "ACCEPTED", "DELIVERED", "OPENED", "REPLIED", "REPLY RATE", "POSITIVE"], rows)];
|
|
603
|
+
const lines = ["", ...renderTextTable(["CHANNEL", "PEOPLE", "SENDS", "ACCEPTED", "DELIVERED", ...(showOpened ? ["OPENED"] : []), "REPLIED", "REPLY RATE", "POSITIVE"], rows)];
|
|
595
604
|
// Name the remedy once per distinct gap rather than once per row.
|
|
596
605
|
const remedies = new Map();
|
|
597
606
|
for (const channel of channels) {
|
|
@@ -688,6 +697,7 @@ function formatSequenceStatsHealth(data) {
|
|
|
688
697
|
["Replies", numberText(stats.totalReplies ?? stats.replied)],
|
|
689
698
|
]));
|
|
690
699
|
}
|
|
700
|
+
lines.push(...formatNewsletterStatsLines(data.newsletter));
|
|
691
701
|
const link = stringValue(data.web_url) ?? stringValue(data.deepLink);
|
|
692
702
|
if (link)
|
|
693
703
|
lines.push("", link);
|
|
@@ -760,7 +770,12 @@ function operationalStateText(sequence, operational) {
|
|
|
760
770
|
if (!state)
|
|
761
771
|
return null;
|
|
762
772
|
// The server's own `lifecycle`; the shared helper covers a server that predates it.
|
|
763
|
-
const
|
|
773
|
+
const settings = isRecord(sequence.settings) ? sequence.settings : null;
|
|
774
|
+
const lifecycle = stringValue(sequence.lifecycle) ?? sequenceDisplayStatus(stringValue(sequence.status) ?? "", state, settings);
|
|
775
|
+
if (lifecycle === "scheduled") {
|
|
776
|
+
const startAt = stringValue(settings?.start_at);
|
|
777
|
+
return `${state} (scheduled${startAt ? ` for ${startAt}` : ""})`;
|
|
778
|
+
}
|
|
764
779
|
return lifecycle === "completed" ? `${state} (completed)` : state;
|
|
765
780
|
}
|
|
766
781
|
/** A listed Sequence's launch warnings (e.g. two sending schedules), one line each. */
|
|
@@ -1342,7 +1357,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1342
1357
|
.description("List sequences with persisted launch status kept separate from current operational state. Fleet counts report marked-active versus operationally-live (active with nonterminal work), and rows explain flowing, paced, paused, blocked, degraded, drained, or empty work without SQL. An active sequence whose every lead has finished shows drained (completed); leads enrolled into it later are contacted on its next dispatch tick. --stats joins each row's LIFETIME funnel.")
|
|
1343
1358
|
.option("--status <status>", "Filter by status: draft, active, paused, or archived.")
|
|
1344
1359
|
.addOption(new Option("--operational-state <state>", "Filter by current work state; fleet counts remain unfiltered: empty, drained, never_started (enrolled leads, never launched), blocked, degraded, flowing, paced, or paused (work waiting on a paused sequence).").choices(["empty", "drained", "never_started", "blocked", "degraded", "flowing", "paced", "paused"]))
|
|
1345
|
-
.addOption(new Option("--lifecycle <lifecycle>", "Filter by the lifecycle a person reads; fleet counts remain unfiltered: draft, active, completed (active, every lead finished), paused, or archived.").choices([...SEQUENCE_LIFECYCLES]))
|
|
1360
|
+
.addOption(new Option("--lifecycle <lifecycle>", "Filter by the lifecycle a person reads; fleet counts remain unfiltered: draft, scheduled (active, start_at still ahead), active, completed (active, every lead finished), paused, or archived.").choices([...SEQUENCE_LIFECYCLES]))
|
|
1346
1361
|
.addOption(new Option("--channel <channel>", "Only sequences carrying this channel.").choices(["email", "linkedin", "whatsapp"]))
|
|
1347
1362
|
.option("--tag <tag>", "Only sequences carrying this workspace tag (see `oxygen tags list`).")
|
|
1348
1363
|
.option("--stats", "Attach lifetime stats per sequence (a stats pass per row — slower on large workspaces)."), SEQUENCE_LIST_PROJECTION_CONTRACT)
|
|
@@ -1672,8 +1687,8 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1672
1687
|
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
1673
1688
|
.option("--auto-optimize", "Enable the sequence-wide A/B auto-winner (default on).")
|
|
1674
1689
|
.option("--no-auto-optimize", "Disable the sequence-wide A/B auto-winner; manual variant decisions stay available.")
|
|
1675
|
-
.option("--reply-to-stop-text <text>", "Optional first-touch email footer (max 500 characters);
|
|
1676
|
-
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as the Sending settings switch: a visible
|
|
1690
|
+
.option("--reply-to-stop-text <text>", "Optional first-touch email footer (max 500 characters); with the unsubscribe option on, plain-text emails show it in place of the link. Empty clears.")
|
|
1691
|
+
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as the Sending settings switch: a visible opt-out (HTML a linked Unsubscribe label; plain text the --reply-to-stop-text line, else a default opt-out line, never the URL) plus one-click headers that mail apps show as an Unsubscribe button, both on first-touch cold emails; in-thread follow-ups carry neither. Zapbox sends (mailboxes imported from Zapmail, `transport: \"zapbox\"` in `mailboxes list --json`) carry the link only. Off by default. Use --reply-to-stop-text for an optional first-touch email footer. `sequences update` can switch on the link or the headers alone.")
|
|
1677
1692
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
1678
1693
|
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
1679
1694
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
@@ -1822,12 +1837,12 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1822
1837
|
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
1823
1838
|
.option("--auto-optimize", "Turn the A/B auto-winner on (the default): every step with variants is judged on replies, pausing losers once each variant has 100 sends and 5 replies, unless the step sets its own auto_optimize.")
|
|
1824
1839
|
.option("--no-auto-optimize", "Disable the sequence-wide A/B auto-winner; manual variant decisions stay available.")
|
|
1825
|
-
.option("--reply-to-stop-text <text>", "Optional first-touch email footer (max 500 characters);
|
|
1826
|
-
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as the Sending settings switch: a visible
|
|
1840
|
+
.option("--reply-to-stop-text <text>", "Optional first-touch email footer (max 500 characters); with the unsubscribe option on, plain-text emails show it in place of the link. Empty clears.")
|
|
1841
|
+
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as the Sending settings switch: a visible opt-out (HTML a linked Unsubscribe label; plain text the --reply-to-stop-text line, else a default opt-out line, never the URL) plus one-click headers that mail apps show as an Unsubscribe button, both on first-touch cold emails; in-thread follow-ups carry neither. Zapbox sends (mailboxes imported from Zapmail, `transport: \"zapbox\"` in `mailboxes list --json`) carry the link only. Off by default. Use --reply-to-stop-text for an optional first-touch email footer.")
|
|
1827
1842
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
1828
1843
|
.option("--include-unsubscribe-headers", "Only the one-click unsubscribe headers (default off); --include-unsubscribe turns on the headers and the visible link together.")
|
|
1829
1844
|
.option("--no-include-unsubscribe-headers", "Omit one-click headers.")
|
|
1830
|
-
.option("--include-unsubscribe-link", "Only the visible recipient-bound unsubscribe footer on first-touch cold emails (default off; --include-unsubscribe adds the one-click headers too).
|
|
1845
|
+
.option("--include-unsubscribe-link", "Only the visible recipient-bound unsubscribe footer on first-touch cold emails (default off; --include-unsubscribe adds the one-click headers too). HTML messages show a linked Unsubscribe label; plain-text messages show the --reply-to-stop-text line, else a default opt-out line, instead of the URL.")
|
|
1831
1846
|
.option("--no-include-unsubscribe-link", "Turn the visible unsubscribe footer OFF. Future first-touch emails send the authored body unchanged; existing suppression records remain enforced.")
|
|
1832
1847
|
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
1833
1848
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
@@ -2038,6 +2053,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2038
2053
|
.option("--max-live-sends <n>", "Total live-send and CRM-write ceiling across channels, including LinkedIn invites/messages (positive integer). Required for email, WhatsApp, or crm_task steps; guided LinkedIn binds its positive cap in launch_review.")
|
|
2039
2054
|
.option("--launch-review-json <json>", "Guided onboarding: echo the exact preview.launch_review (digest, expiry, positive action ceiling and nonnegative credit cap). Changed inputs require a new preview.")
|
|
2040
2055
|
.option("--launch-review-file <path>", "Read the exact launch_review object from a JSON file instead of --launch-review-json.")
|
|
2056
|
+
.option("--at <time>", "Schedule a draft's start: an ISO timestamp with a timezone (2026-11-02T09:00:00Z) or \"in 30m\" / \"in 2h\" / \"in 3d\", at most 30 days ahead. With --approved it activates now and sends nothing before then (lifecycle scheduled); cancel with `sequences unschedule`.")
|
|
2041
2057
|
.option("--dry-run", "Activate the sequence (status draft → active, startedAt set) in dry-run mode: every step advances with simulated actions — no sends, CRM writes, provider calls, or credits. This is NOT the preview: `sequences start` with no flags is the side-effect-free launch preview. A later live start (--approved) restarts those leads from the first step so they are contacted for real, except deleted source rows, a person another live campaign contacts under exclude_contacted, and older duplicates of a person.")
|
|
2042
2058
|
.option("--json", "Print a JSON envelope.")
|
|
2043
2059
|
.action(async (sequence, options) => {
|
|
@@ -2051,6 +2067,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2051
2067
|
: options.launchReviewJson !== undefined ? parseJsonObject(options.launchReviewJson) : undefined;
|
|
2052
2068
|
if (launchReview !== undefined && !isRecord(launchReview))
|
|
2053
2069
|
throw new OxygenError("invalid_request", "The launch review must be the exact object returned by the preview.", { exitCode: 2 });
|
|
2070
|
+
const startAt = options.at === undefined ? undefined : parseStartAtFlag(options.at);
|
|
2054
2071
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
2055
2072
|
method: "POST",
|
|
2056
2073
|
body: {
|
|
@@ -2059,6 +2076,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2059
2076
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
2060
2077
|
...(maxLiveSends !== undefined ? { max_live_sends: maxLiveSends } : {}),
|
|
2061
2078
|
...(launchReview !== undefined ? { launch_review: launchReview } : {}),
|
|
2079
|
+
...(startAt !== undefined ? { start_at: startAt } : {}),
|
|
2062
2080
|
},
|
|
2063
2081
|
});
|
|
2064
2082
|
});
|
|
@@ -2147,6 +2165,15 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2147
2165
|
await handleAsyncAction("sequences pause", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
2148
2166
|
method: "POST", body: { status: "paused" },
|
|
2149
2167
|
}));
|
|
2168
|
+
}))
|
|
2169
|
+
.addCommand(new Command("unschedule")
|
|
2170
|
+
.description("Cancel a scheduled start (`sequences start --at`) before it comes: the Sequence returns to draft, its leads to pending, and the start time is cleared. Refused once the start has come or anything was planned; pause it then. Repeating it changes nothing.")
|
|
2171
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
2172
|
+
.option("--json", "Print a JSON envelope.")
|
|
2173
|
+
.action(async (sequence, options) => {
|
|
2174
|
+
await handleAsyncAction("sequences unschedule", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/unschedule`, {
|
|
2175
|
+
method: "POST", body: {},
|
|
2176
|
+
}));
|
|
2150
2177
|
}))
|
|
2151
2178
|
.addCommand(new Command("resume")
|
|
2152
2179
|
.description("Resume a paused sequence. A campaign paused at its live send cap pauses again at once: raise the cap and resume in one step with `sequences start <sequence> --max-live-sends <n>` (preview, then --approved).")
|
|
@@ -2440,12 +2467,17 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2440
2467
|
});
|
|
2441
2468
|
}))
|
|
2442
2469
|
.addCommand(new Command("stats")
|
|
2443
|
-
.description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel. Today's completed sends, reservations and remaining capacity appear in the default output and daily_budget. operational.reasons carries why work is waiting (outside_send_window, mailbox_spacing, daily_send_cap_reached, ...). For a per-day send series use `sequences analytics --sequence <id> --range <window>`.")
|
|
2470
|
+
.description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel. Today's completed sends, reservations and remaining capacity appear in the default output and daily_budget. operational.reasons carries why work is waiting (outside_send_window, mailbox_spacing, daily_send_cap_reached, ...). For a per-day send series use `sequences analytics --sequence <id> --range <window>`. A newsletter (sending-domain Sequence) adds a newsletter block: delivered, estimated opens, clicks per link, bounces, complaints, unsubscribes and a first-week timeline; per-recipient status is `sequences recipients`.")
|
|
2444
2471
|
.argument("<sequence>", "Sequence id or slug.")
|
|
2472
|
+
.option("--interval <hour|day>", "Newsletter timeline buckets over the first 7 days (default day).")
|
|
2445
2473
|
.option("--json", "Print a JSON envelope.")
|
|
2446
2474
|
.action(async (sequence, options) => {
|
|
2447
|
-
await handleReadActionWithLens("sequences stats", options, () =>
|
|
2475
|
+
await handleReadActionWithLens("sequences stats", options, () => {
|
|
2476
|
+
const interval = readNewsletterIntervalFlag(options.interval);
|
|
2477
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats${interval ? `?interval=${interval}` : ""}`);
|
|
2478
|
+
}, formatSequenceStatsHealth);
|
|
2448
2479
|
}))
|
|
2480
|
+
.addCommand(sequencesRecipientsCommand())
|
|
2449
2481
|
.addCommand(new Command("esp")
|
|
2450
2482
|
.description("ESP breakdown of this sequence's email sending scope: which provider each enrolled lead's mail routes through (google/microsoft, plus named gateways like Proofpoint/Mimecast and non-matchable hosts), folded against your sending mailbox pool. Shows how many leads the CURRENT --esp-matching mode would defer before you turn it on. Resolves domains from MX + SPF (0 credits, DNS only) and caches the result, warming the send path. Preview before flipping strict/prefer.")
|
|
2451
2483
|
.argument("<sequence>", "Sequence id or slug.")
|
|
@@ -2480,5 +2512,8 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
2480
2512
|
.option("--json", "Print a JSON envelope.")
|
|
2481
2513
|
.action(async (sequence, options) => {
|
|
2482
2514
|
await handleSequenceVariantsAction(sequence, options);
|
|
2483
|
-
}))
|
|
2515
|
+
}))
|
|
2516
|
+
.addCommand(sequencesContentCommand(handleAsyncAction))
|
|
2517
|
+
.addCommand(sequencesBrandKitCommand(handleAsyncAction))
|
|
2518
|
+
.addCommand(sequencesTestSendCommand(handleAsyncAction)));
|
|
2484
2519
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { ExtractedCommandActionHandler } from "../command-support.js";
|
|
2
|
+
import { type EmailContactImportRow } from "@oxygen/shared/email-subscriptions";
|
|
2
3
|
import { Command } from "commander";
|
|
3
|
-
type ImportEntry =
|
|
4
|
-
email: string;
|
|
5
|
-
person_id?: string;
|
|
6
|
-
};
|
|
4
|
+
type ImportEntry = EmailContactImportRow;
|
|
7
5
|
/** Read --file as JSON (by extension or content) or as CSV with an email column. */
|
|
8
6
|
export declare function readSubscriptionImportFile(path: string): ImportEntry[];
|
|
9
7
|
export declare function registerSubscriptionsFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
|