@oxygen-agent/cli 1.883.2 → 1.886.3
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/index.js +27 -3
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +44 -3
- package/node_modules/@oxygen/shared/dist/sequences.js +99 -8
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -1836,15 +1836,23 @@ function writeSequenceDraftPreview(data) {
|
|
|
1836
1836
|
lines.push("Drafted email sequence (preview — not saved):");
|
|
1837
1837
|
}
|
|
1838
1838
|
let emailNumber = 0;
|
|
1839
|
+
// The subject the next threaded email continues under. A drafted follow-up
|
|
1840
|
+
// deliberately has no subject of its own — that is what keeps it in the first
|
|
1841
|
+
// email's thread — so show what it actually goes out under rather than the
|
|
1842
|
+
// "(no subject)" that reads like a drafting failure.
|
|
1843
|
+
let threadSubject = null;
|
|
1839
1844
|
for (const step of steps) {
|
|
1840
1845
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
1841
1846
|
continue;
|
|
1842
1847
|
const entry = step;
|
|
1843
1848
|
if (entry.kind === "email_send") {
|
|
1844
1849
|
emailNumber += 1;
|
|
1845
|
-
const
|
|
1850
|
+
const own = typeof entry.subject_template === "string" && entry.subject_template.trim()
|
|
1846
1851
|
? entry.subject_template.trim()
|
|
1847
|
-
:
|
|
1852
|
+
: null;
|
|
1853
|
+
if (own)
|
|
1854
|
+
threadSubject = own;
|
|
1855
|
+
const subject = own ?? (threadSubject ? `Re: ${threadSubject} (same thread)` : "(no subject)");
|
|
1848
1856
|
const variantCount = Array.isArray(entry.variants) ? entry.variants.length : 0;
|
|
1849
1857
|
lines.push(` Email ${emailNumber}: ${subject}${variantCount > 0 ? ` (+${variantCount} A/B variant${variantCount === 1 ? "" : "s"})` : ""}`);
|
|
1850
1858
|
}
|
|
@@ -9495,6 +9503,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9495
9503
|
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
9496
9504
|
.option("--title <text>", "Optional human title for the session.")
|
|
9497
9505
|
.option("--auto-approve", "Start with auto-approve ON for paid, workspace-internal actions (cards are still created and decided automatically within each action's credit cap). External sends, enrollments, publishes, DNS, and external CRM pushes always stay human-gated.")
|
|
9506
|
+
.option("--no-follow", "Start with screen-follow OFF. On by default: while the session is open in the browser, your screen opens onto whatever the copilot creates or changes, with the live session docked beside it. Change it later with `copilot follow`.")
|
|
9498
9507
|
.option("--json", "Print a JSON envelope.")
|
|
9499
9508
|
.action(async (options) => {
|
|
9500
9509
|
await handleAsyncAction("copilot start", options, () => {
|
|
@@ -9520,6 +9529,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9520
9529
|
body.title = title;
|
|
9521
9530
|
if (options.autoApprove)
|
|
9522
9531
|
body.auto_approve = true;
|
|
9532
|
+
// Commander maps --no-follow to `follow: false` (default true), and
|
|
9533
|
+
// the server already defaults to following, so only the opt-out ships.
|
|
9534
|
+
if (options.follow === false)
|
|
9535
|
+
body.surface_policy = "off";
|
|
9523
9536
|
return requestOxygen("/api/cli/copilot/sessions", { method: "POST", body });
|
|
9524
9537
|
});
|
|
9525
9538
|
}))
|
|
@@ -9547,6 +9560,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9547
9560
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
9548
9561
|
return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}${suffix}`);
|
|
9549
9562
|
});
|
|
9563
|
+
}))
|
|
9564
|
+
.addCommand(new Command("follow")
|
|
9565
|
+
.description("Turn screen-follow ON (or, with --off, OFF) for an open Workspace Copilot session: while it is open in the browser, your screen opens onto whatever the copilot creates or changes, with the live session docked beside it. Same switch as the web composer's \"Show me\" toggle; changes nothing about what the session is allowed to do.")
|
|
9566
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
9567
|
+
.option("--off", "Stop following: the copilot keeps working, your screen just stays where it is.")
|
|
9568
|
+
.option("--json", "Print a JSON envelope.")
|
|
9569
|
+
.action(async (sessionId, options) => {
|
|
9570
|
+
await handleAsyncAction("copilot follow", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}`, {
|
|
9571
|
+
method: "PATCH",
|
|
9572
|
+
body: { surface_policy: options.off ? "off" : "follow" },
|
|
9573
|
+
}));
|
|
9550
9574
|
}))
|
|
9551
9575
|
.addCommand(new Command("send")
|
|
9552
9576
|
.description("Send a message to a Workspace Copilot session and stream the reply to stderr. Waits for the turn to finish (or pause for an approval) unless --no-wait. Inference bills credits at 5x the actual model cost within the session budget.")
|
|
@@ -12354,7 +12378,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12354
12378
|
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys); bind an Instantly email track with --email-*. Install/read the oxygen-sequencer skill for complete mapped CRM task examples.")
|
|
12355
12379
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
12356
12380
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
12357
|
-
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send |
|
|
12381
|
+
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Native email sends (email_send) also expose three reserved sender variables from the sending mailbox: {{sender_name}} (mailbox display name), {{sender_first_name}} (its first word), and {{sender_email}} (the from address); a row column of the same name WINS on collision, and a missing display name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread (what the retired email_reply kind used to be — still accepted on input and rewritten into this shape). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue; A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, install/read the oxygen-sequencer skill.")
|
|
12358
12382
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp,call,crm. Defaults to the channels the journey touches. crm_task currently supports HubSpot.")
|
|
12359
12383
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live — WhatsApp via Unipile is unofficial WhatsApp Web, so cold-initiating is an explicit ban-risk opt-in.")
|
|
12360
12384
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
@@ -426,8 +426,21 @@ export type SequenceEmailSendStep = {
|
|
|
426
426
|
id: string;
|
|
427
427
|
channel: "email";
|
|
428
428
|
kind: "email_send";
|
|
429
|
-
/**
|
|
430
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Subject line. Supports {{column}} interpolation.
|
|
431
|
+
*
|
|
432
|
+
* OPTIONAL, and that option IS the one-email-action model: writing a subject
|
|
433
|
+
* opens a NEW thread, leaving it blank CONTINUES the lead's existing one. The
|
|
434
|
+
* builder pre-fills a follow-up step with the previous email's subject, so
|
|
435
|
+
* leaving it alone threads and typing a different one forks a new email. Blank
|
|
436
|
+
* or absent therefore means exactly what the retired `email_reply` kind meant,
|
|
437
|
+
* and a stored legacy `email_reply` normalizes into precisely this shape.
|
|
438
|
+
*
|
|
439
|
+
* The FIRST native email step has no thread to continue, so it must carry a
|
|
440
|
+
* subject — enforced by validateSequenceDefinition (and reported as missing
|
|
441
|
+
* copy on a draft, which is allowed to be half-written).
|
|
442
|
+
*/
|
|
443
|
+
subject_template?: string;
|
|
431
444
|
/** Body (plain text). Supports {{column}} interpolation. */
|
|
432
445
|
body_template: string;
|
|
433
446
|
/** Optional HTML body (B-W2): renders the send as multipart/alternative. Supports {{column}}. */
|
|
@@ -446,7 +459,15 @@ export type SequenceEmailSendStep = {
|
|
|
446
459
|
/** Per-step send window (overrides the sequence-level email_send_window). */
|
|
447
460
|
send_window?: SequenceSendWindow;
|
|
448
461
|
};
|
|
449
|
-
/**
|
|
462
|
+
/**
|
|
463
|
+
* LEGACY threaded follow-up, retired as an authored step kind.
|
|
464
|
+
*
|
|
465
|
+
* "Reply in thread" merged into "Send email": a subject-less `email_send` IS the
|
|
466
|
+
* threaded follow-up. normalizeStep rewrites any incoming `email_reply` into that
|
|
467
|
+
* shape, so no newly written definition contains this kind — but definitions
|
|
468
|
+
* stored before the merge do, and the planner/dispatcher still execute them
|
|
469
|
+
* unchanged, so the type stays.
|
|
470
|
+
*/
|
|
450
471
|
export type SequenceEmailReplyStep = {
|
|
451
472
|
id: string;
|
|
452
473
|
channel: "email";
|
|
@@ -468,6 +489,26 @@ export type SequenceEmailReplyStep = {
|
|
|
468
489
|
/** Per-step send window (overrides the sequence-level email_send_window). */
|
|
469
490
|
send_window?: SequenceSendWindow;
|
|
470
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* Does this email step continue the lead's existing thread instead of opening a
|
|
494
|
+
* new one? True for a subject-less `email_send` (the merged model) and for a
|
|
495
|
+
* stored legacy `email_reply`. The single predicate every surface reads: the
|
|
496
|
+
* planner (which action kind to materialize), the send payload, the canvas, and
|
|
497
|
+
* the builder's subject field.
|
|
498
|
+
*/
|
|
499
|
+
export declare function sequenceEmailStepThreads(step: SequenceStep): boolean;
|
|
500
|
+
/**
|
|
501
|
+
* The subject a threaded email step inherits: the nearest PRECEDING email step
|
|
502
|
+
* that opens a thread of its own, in flat step order. Flat order is the order the
|
|
503
|
+
* dispatcher's "latest email message ref for this enrollment" resolves in, so
|
|
504
|
+
* this is the subject the send actually lands under. Null when nothing precedes
|
|
505
|
+
* it — the state validateSequenceDefinition rejects, because a threaded send with
|
|
506
|
+
* no thread would go out with no subject at all.
|
|
507
|
+
*/
|
|
508
|
+
export declare function sequenceInheritedEmailSubject(steps: readonly {
|
|
509
|
+
readonly kind: string;
|
|
510
|
+
readonly subject_template?: string;
|
|
511
|
+
}[], index: number): string | null;
|
|
471
512
|
/**
|
|
472
513
|
* Native WhatsApp message send through one of the sequence's WhatsApp senders
|
|
473
514
|
* (Unipile chats_messages_send into an existing chat, else chats_create to open
|
|
@@ -629,6 +629,43 @@ export const SEQUENCE_STEP_KINDS = [
|
|
|
629
629
|
"branch",
|
|
630
630
|
"stop",
|
|
631
631
|
];
|
|
632
|
+
/**
|
|
633
|
+
* Does this email step continue the lead's existing thread instead of opening a
|
|
634
|
+
* new one? True for a subject-less `email_send` (the merged model) and for a
|
|
635
|
+
* stored legacy `email_reply`. The single predicate every surface reads: the
|
|
636
|
+
* planner (which action kind to materialize), the send payload, the canvas, and
|
|
637
|
+
* the builder's subject field.
|
|
638
|
+
*/
|
|
639
|
+
export function sequenceEmailStepThreads(step) {
|
|
640
|
+
if (step.kind === "email_reply")
|
|
641
|
+
return true;
|
|
642
|
+
if (step.kind !== "email_send")
|
|
643
|
+
return false;
|
|
644
|
+
return !(step.subject_template ?? "").trim();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* The subject a threaded email step inherits: the nearest PRECEDING email step
|
|
648
|
+
* that opens a thread of its own, in flat step order. Flat order is the order the
|
|
649
|
+
* dispatcher's "latest email message ref for this enrollment" resolves in, so
|
|
650
|
+
* this is the subject the send actually lands under. Null when nothing precedes
|
|
651
|
+
* it — the state validateSequenceDefinition rejects, because a threaded send with
|
|
652
|
+
* no thread would go out with no subject at all.
|
|
653
|
+
*/
|
|
654
|
+
export function sequenceInheritedEmailSubject(
|
|
655
|
+
// Structural rather than SequenceStep[]: the planner walks a looser PlanStep[]
|
|
656
|
+
// (it accepts channel-less LinkedIn steps), and this only ever reads kind +
|
|
657
|
+
// subject_template.
|
|
658
|
+
steps, index) {
|
|
659
|
+
for (let i = Math.min(index, steps.length) - 1; i >= 0; i -= 1) {
|
|
660
|
+
const prior = steps[i];
|
|
661
|
+
if (!prior || prior.kind !== "email_send")
|
|
662
|
+
continue;
|
|
663
|
+
const subject = prior.subject_template ?? "";
|
|
664
|
+
if (subject.trim())
|
|
665
|
+
return subject;
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
632
669
|
// ---- CRM-channel steps (external writes through a connected CRM) ----
|
|
633
670
|
export const SEQUENCE_CRM_TASK_PROVIDERS = ["hubspot"];
|
|
634
671
|
export const SEQUENCE_CRM_IDENTITY_KINDS = [
|
|
@@ -686,7 +723,7 @@ export function sequenceStepsMissingCopy(steps) {
|
|
|
686
723
|
if (typeof value !== "string" || !value.trim())
|
|
687
724
|
missing.push({ stepId: step.id, kind: step.kind, field });
|
|
688
725
|
};
|
|
689
|
-
|
|
726
|
+
steps.forEach((step, index) => {
|
|
690
727
|
switch (step.kind) {
|
|
691
728
|
case "message":
|
|
692
729
|
case "whatsapp_message":
|
|
@@ -697,7 +734,13 @@ export function sequenceStepsMissingCopy(steps) {
|
|
|
697
734
|
check(step, "template", step.template);
|
|
698
735
|
break;
|
|
699
736
|
case "email_send":
|
|
700
|
-
|
|
737
|
+
// A blank subject is only "missing copy" on the FIRST email of a journey.
|
|
738
|
+
// Anywhere after one, blank is a deliberate choice — continue the previous
|
|
739
|
+
// thread — and demanding a subject there would block the launch of exactly
|
|
740
|
+
// the follow-up the merged action exists to make one click.
|
|
741
|
+
if (sequenceInheritedEmailSubject(steps, index) === null) {
|
|
742
|
+
check(step, "subject_template", step.subject_template);
|
|
743
|
+
}
|
|
701
744
|
check(step, "body_template", step.body_template);
|
|
702
745
|
break;
|
|
703
746
|
case "email_reply":
|
|
@@ -730,7 +773,7 @@ export function sequenceStepsMissingCopy(steps) {
|
|
|
730
773
|
default:
|
|
731
774
|
break;
|
|
732
775
|
}
|
|
733
|
-
}
|
|
776
|
+
});
|
|
734
777
|
return missing;
|
|
735
778
|
}
|
|
736
779
|
/**
|
|
@@ -788,6 +831,35 @@ export function validateSequenceDefinition(input, options = {}) {
|
|
|
788
831
|
if (!options.nativeTrackingEnabled) {
|
|
789
832
|
reportNativeEngagementGates(normalized, issues);
|
|
790
833
|
}
|
|
834
|
+
// Pass 4: a threaded email step needs a thread to continue. "Send email" reads
|
|
835
|
+
// its subject as the fork: written => new thread, blank => continue the previous
|
|
836
|
+
// email. The first email of a journey has no previous email, so a blank subject
|
|
837
|
+
// there is not a follow-up, it is a cold send that would go out with no Subject
|
|
838
|
+
// header at all. Reject it on a real write; a draft is allowed to be half-typed
|
|
839
|
+
// and surfaces the same gap through sequenceStepsMissingCopy at launch.
|
|
840
|
+
//
|
|
841
|
+
// Which "earlier" applies depends on the journey. A LINEAR journey runs in flat
|
|
842
|
+
// array order, so flat order is exactly what precedes a step. A BRANCHED one does
|
|
843
|
+
// not: an arm may route backwards, so a step that reads as first in the array can
|
|
844
|
+
// still be reached after an email that opened a thread — and the dispatcher
|
|
845
|
+
// resolves the thread from what this enrollment actually SENT, not from step
|
|
846
|
+
// order. Being stricter than the runtime there would refuse to save a campaign
|
|
847
|
+
// that works, so a branched journey only has to open a thread somewhere.
|
|
848
|
+
if (options.draft !== true) {
|
|
849
|
+
const branched = normalized.some((step) => step.kind === "branch");
|
|
850
|
+
const opensAnyThread = normalized.some((step) => step.kind === "email_send" && !sequenceEmailStepThreads(step));
|
|
851
|
+
normalized.forEach((step, index) => {
|
|
852
|
+
if (step.kind !== "email_send" || !sequenceEmailStepThreads(step))
|
|
853
|
+
return;
|
|
854
|
+
const hasThread = branched ? opensAnyThread : sequenceInheritedEmailSubject(normalized, index) !== null;
|
|
855
|
+
if (hasThread)
|
|
856
|
+
return;
|
|
857
|
+
issues.push({
|
|
858
|
+
path: `steps[${index}].subject_template`,
|
|
859
|
+
message: `step '${step.id}' has no subject, which means "continue the previous email" — but no earlier step sends one. Give the first email of the sequence a subject.`,
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
}
|
|
791
863
|
if (issues.length > 0) {
|
|
792
864
|
throw new OxygenError("invalid_sequence", `Sequence definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`, { details: { issues }, exitCode: 1 });
|
|
793
865
|
}
|
|
@@ -953,16 +1025,26 @@ raw, index, options, issues) {
|
|
|
953
1025
|
return { id, channel: "email", kind: "email_move", subsequence_id: subsequenceId ?? "" };
|
|
954
1026
|
}
|
|
955
1027
|
case "email_send": {
|
|
956
|
-
|
|
1028
|
+
// Subject is optional. Present => a new thread; absent/blank => continue the
|
|
1029
|
+
// lead's existing one (what `email_reply` used to be). A blank one is dropped
|
|
1030
|
+
// rather than stored as "", so the definition states the intent structurally
|
|
1031
|
+
// instead of relying on an empty string being read the right way downstream.
|
|
1032
|
+
const subject = optionalTemplate(raw.subject_template, `${path}.subject_template`, MAX_TEMPLATE_LENGTH, issues);
|
|
1033
|
+
const threaded = !(subject ?? "").trim();
|
|
957
1034
|
const body = requiredTemplate(raw.body_template, `${path}.body_template`, issues, options.draft === true);
|
|
958
1035
|
const bodyHtml = optionalTemplate(raw.body_html_template, `${path}.body_html_template`, MAX_TEMPLATE_LENGTH, issues);
|
|
959
|
-
|
|
1036
|
+
// A threaded step has no subject to override, so its variants may only vary
|
|
1037
|
+
// the body — a subject-only variant then fails normalizeVariants' own
|
|
1038
|
+
// "must override at least one of" check rather than silently doing nothing.
|
|
1039
|
+
const variants = normalizeVariants(raw.variants, `${path}.variants`, threaded ? ["body_template", "body_html_template"] : ["subject_template", "body_template", "body_html_template"], issues, options.draft === true);
|
|
960
1040
|
const autoOptimize = normalizeAutoOptimize(raw.auto_optimize, `${path}.auto_optimize`, variants, options, issues);
|
|
961
1041
|
const cc = optionalEmailList(raw.cc, `${path}.cc`, issues);
|
|
962
1042
|
const bcc = optionalEmailList(raw.bcc, `${path}.bcc`, issues);
|
|
963
1043
|
const sendWindow = normalizeSendWindow(raw.send_window, `${path}.send_window`, issues);
|
|
964
1044
|
return {
|
|
965
|
-
id, channel: "email", kind: "email_send",
|
|
1045
|
+
id, channel: "email", kind: "email_send",
|
|
1046
|
+
...(threaded ? {} : { subject_template: subject }),
|
|
1047
|
+
body_template: body ?? "",
|
|
966
1048
|
...(bodyHtml !== undefined ? { body_html_template: bodyHtml } : {}),
|
|
967
1049
|
...(cc ? { cc } : {}),
|
|
968
1050
|
...(bcc ? { bcc } : {}),
|
|
@@ -972,6 +1054,13 @@ raw, index, options, issues) {
|
|
|
972
1054
|
};
|
|
973
1055
|
}
|
|
974
1056
|
case "email_reply": {
|
|
1057
|
+
// Retired as an authored kind: "Reply in thread" merged into "Send email",
|
|
1058
|
+
// where a subject-less step IS the threaded follow-up. Anything still writing
|
|
1059
|
+
// `email_reply` — an older CLI, a recipe file, a stored definition being
|
|
1060
|
+
// re-saved — migrates forward here, so every newly written definition holds
|
|
1061
|
+
// exactly one email-send kind. Definitions that are never re-saved keep the
|
|
1062
|
+
// old kind on disk and keep executing: the planner and the dispatcher both
|
|
1063
|
+
// still handle it, and it plans byte-identically to the threaded form.
|
|
975
1064
|
const body = requiredTemplate(raw.body_template, `${path}.body_template`, issues, options.draft === true);
|
|
976
1065
|
const bodyHtml = optionalTemplate(raw.body_html_template, `${path}.body_html_template`, MAX_TEMPLATE_LENGTH, issues);
|
|
977
1066
|
const variants = normalizeVariants(raw.variants, `${path}.variants`, ["body_template", "body_html_template"], issues, options.draft === true);
|
|
@@ -980,7 +1069,7 @@ raw, index, options, issues) {
|
|
|
980
1069
|
const bcc = optionalEmailList(raw.bcc, `${path}.bcc`, issues);
|
|
981
1070
|
const sendWindow = normalizeSendWindow(raw.send_window, `${path}.send_window`, issues);
|
|
982
1071
|
return {
|
|
983
|
-
id, channel: "email", kind: "
|
|
1072
|
+
id, channel: "email", kind: "email_send", body_template: body ?? "",
|
|
984
1073
|
...(bodyHtml !== undefined ? { body_html_template: bodyHtml } : {}),
|
|
985
1074
|
...(cc ? { cc } : {}),
|
|
986
1075
|
...(bcc ? { bcc } : {}),
|
|
@@ -2014,7 +2103,9 @@ export function sequenceVariantLabel(index) {
|
|
|
2014
2103
|
function baseVariantContent(step) {
|
|
2015
2104
|
switch (step.kind) {
|
|
2016
2105
|
case "email_send": return {
|
|
2017
|
-
|
|
2106
|
+
// Omitted on a threaded step: there is no subject to vary, and emitting ""
|
|
2107
|
+
// would make sequenceTemplateVariables scan a field the send never renders.
|
|
2108
|
+
...(step.subject_template ? { subject_template: step.subject_template } : {}),
|
|
2018
2109
|
body_template: step.body_template,
|
|
2019
2110
|
// Include the HTML body so its {{column}} refs are scanned by
|
|
2020
2111
|
// sequenceTemplateVariables (the start preview's variable-resolution check).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.886.3";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
3
3
|
export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
|
|
4
4
|
export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.886.3";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|