@oxygen-agent/cli 1.377.3 → 1.591.1

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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/column-run-notices.d.ts +11 -0
  3. package/dist/column-run-notices.js +37 -0
  4. package/dist/command-manifest.js +13 -8
  5. package/dist/help.js +79 -16
  6. package/dist/index.js +3812 -460
  7. package/dist/skills.js +106 -1
  8. package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
  9. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  10. package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
  11. package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
  12. package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
  13. package/node_modules/@oxygen/formula/dist/expression.js +428 -0
  14. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
  15. package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
  16. package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
  17. package/node_modules/@oxygen/formula/dist/index.js +17 -0
  18. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
  19. package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
  20. package/node_modules/@oxygen/formula/package.json +26 -0
  21. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
  22. package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
  25. package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
  26. package/node_modules/@oxygen/shared/dist/billing.js +185 -8
  27. package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
  28. package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
  29. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
  31. package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
  32. package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
  33. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
  34. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/directory.js +1 -0
  37. package/node_modules/@oxygen/shared/dist/file-import.js +58 -11
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
  39. package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
  40. package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
  41. package/node_modules/@oxygen/shared/dist/index.js +9 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
  44. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
  45. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
  46. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  47. package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
  48. package/node_modules/@oxygen/shared/dist/log.js +41 -2
  49. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
  50. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
  51. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
  52. package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
  53. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
  54. package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
  55. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
  56. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
  57. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
  59. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
  60. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  61. package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
  62. package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
  63. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
  64. package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
  65. package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
  66. package/node_modules/@oxygen/shared/dist/tags.js +122 -6
  67. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  68. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  69. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
  70. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
  71. package/node_modules/@oxygen/shared/package.json +95 -0
  72. package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
  73. package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
  74. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
  75. package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
  76. package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
  77. package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
  78. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
  79. package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
  80. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
  81. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
  84. package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
  85. package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
  86. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
  87. package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
  88. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
  89. package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
  90. package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
  91. package/node_modules/@oxygen/workflows/dist/index.js +179 -13
  92. package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
  93. package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
  94. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
  95. package/node_modules/@oxygen/workflows/package.json +4 -0
  96. package/package.json +7 -5
@@ -27,7 +27,7 @@ import { type RenderTemplateOptions } from "./sequence-template.js";
27
27
  * and inbox rotation while Oxygen owns the cross-channel timeline + reply
28
28
  * suppression.
29
29
  */
30
- export declare const SEQUENCE_CHANNELS: readonly ["linkedin", "email", "whatsapp"];
30
+ export declare const SEQUENCE_CHANNELS: readonly ["linkedin", "email", "whatsapp", "call", "crm"];
31
31
  export type SequenceChannel = (typeof SEQUENCE_CHANNELS)[number];
32
32
  /**
33
33
  * Signals an enrollment accumulates from provider webhooks. wait_for_signal gates
@@ -36,7 +36,7 @@ export type SequenceChannel = (typeof SEQUENCE_CHANNELS)[number];
36
36
  * a signal — it's resolved on demand by the dispatcher via a connection branch,
37
37
  * `condition: "already_connected"`.)
38
38
  */
39
- export declare const SEQUENCE_SIGNALS: readonly ["linkedin_connected", "linkedin_replied", "whatsapp_replied", "email_sent", "email_opened", "email_clicked", "email_replied", "email_bounced", "meeting_booked", "email_unsubscribed", "company_hiring", "company_raised_funds", "job_change", "new_hire", "web_visit", "intent"];
39
+ export declare const SEQUENCE_SIGNALS: readonly ["linkedin_connected", "linkedin_replied", "whatsapp_replied", "email_sent", "email_opened", "email_clicked", "email_replied", "email_bounced", "meeting_booked", "email_unsubscribed", "call_connected", "call_no_answer", "call_voicemail", "company_hiring", "company_raised_funds", "job_change", "new_hire", "web_visit", "intent"];
40
40
  export type SequenceSignal = (typeof SEQUENCE_SIGNALS)[number];
41
41
  /**
42
42
  * Email engagement signals that ONLY arrive via the Instantly webhook
@@ -49,6 +49,27 @@ export type SequenceSignal = (typeof SEQUENCE_SIGNALS)[number];
49
49
  */
50
50
  export declare const SEQUENCE_NATIVE_UNTRACKED_SIGNALS: readonly ["email_opened", "email_clicked"];
51
51
  export type SequenceNativeUntrackedSignal = (typeof SEQUENCE_NATIVE_UNTRACKED_SIGNALS)[number];
52
+ /**
53
+ * Signals that ALWAYS terminalize the enrollment the moment they fire, so no
54
+ * later step can ever observe them. A reply on any channel unconditionally stops
55
+ * the replying lead's enrollment (`status: 'replied'`, reason `lead_replied` —
56
+ * see the LinkedIn / WhatsApp / email inbound paths), and the one-click
57
+ * List-Unsubscribe webhook stops it too (`unsubscribed_one_click`).
58
+ *
59
+ * They are still RECORDED on the enrollment (analytics, the timeline, and the
60
+ * reply-rate rollups all read them), but a branch or wait_for_signal gated on one
61
+ * is dead code: the enrollment is already terminal when the signal lands, so the
62
+ * gate always times out / always takes the `else` arm. The builder therefore does
63
+ * not offer them as branch or gate conditions — reply-stop is built in.
64
+ */
65
+ export declare const SEQUENCE_TERMINAL_SIGNALS: readonly ["linkedin_replied", "whatsapp_replied", "email_replied", "email_unsubscribed"];
66
+ export type SequenceTerminalSignal = (typeof SEQUENCE_TERMINAL_SIGNALS)[number];
67
+ export declare function isTerminalSequenceSignal(value: string): value is SequenceTerminalSignal;
68
+ /**
69
+ * The signals a branch / wait_for_signal condition can meaningfully read — every
70
+ * signal except the terminal ones above. This is what authoring surfaces offer.
71
+ */
72
+ export declare const SEQUENCE_BRANCHABLE_SIGNALS: readonly SequenceSignal[];
52
73
  /** External GTM signals (the non-engagement subset of SEQUENCE_SIGNALS). */
53
74
  export declare const SEQUENCE_EXTERNAL_SIGNALS: readonly ["company_hiring", "company_raised_funds", "job_change", "new_hire", "web_visit", "intent"];
54
75
  export type SequenceExternalSignal = (typeof SEQUENCE_EXTERNAL_SIGNALS)[number];
@@ -67,17 +88,20 @@ export declare const SEQUENCE_EMAIL_COLUMN_KEYS: readonly ["email", "email_addre
67
88
  * complaint loop (the address a bounce/opt-out suppresses), so they can't drift.
68
89
  * Returns the trimmed raw value (callers normalize/lowercase as needed) or null.
69
90
  */
70
- export declare function recipientEmailFromRow(rowValues: Record<string, unknown> | null | undefined): string | null;
91
+ export declare function recipientEmailFromRow(rowValues: Record<string, unknown> | null | undefined, emailColumnKey?: string | null): string | null;
71
92
  /**
72
93
  * ESP (email service provider) MATCHING mode for a sequence (settings.esp_matching).
73
94
  * Deliverability lore: a send lands better when the SENDING mailbox and the
74
95
  * RECIPIENT sit on the same provider (Google→Google, Microsoft→Microsoft), so at
75
96
  * mailbox-pick the dispatcher can bias rotation toward a same-ESP mailbox.
76
- * - "off" — no ESP bias (default); rotation is pure LRU / domain-spread.
77
- * - "prefer" — pick a same-ESP mailbox when one is available, else fall back to
78
- * any sendable mailbox (never blocks a send).
97
+ * - "off" — no ESP bias; rotation is pure LRU / domain-spread.
98
+ * - "prefer" — (DEFAULT) pick a same-ESP mailbox when one is available, else
99
+ * fall back to any sendable mailbox (never blocks a send). On by default
100
+ * because it only ever improves deliverability and cannot starve sends.
79
101
  * - "strict" — require a same-ESP mailbox; when none is available the action is
80
102
  * DEFERRED with a visible skipped_reason rather than sent cross-provider.
103
+ * Power-user setting, reachable via CLI/MCP; the web "Provider Matching"
104
+ * checkbox toggles off↔prefer only.
81
105
  */
82
106
  export declare const ESP_MATCHING_MODES: readonly ["off", "prefer", "strict"];
83
107
  export type EspMatchingMode = (typeof ESP_MATCHING_MODES)[number];
@@ -92,28 +116,78 @@ export declare function isEspMatchingMode(value: unknown): value is EspMatchingM
92
116
  */
93
117
  export declare function validateEspMatchingSetting(value: unknown): void;
94
118
  /**
95
- * The email-provider FAMILY a recipient (or sending mailbox) routes through. Kept
96
- * intentionally aligned with EMAIL_MAILBOX_PROVIDERS in tenant-db so a mailbox's
97
- * `provider` and an inferred recipient ESP compare directly.
119
+ * The MATCHABLE email-provider a recipient routes through — the narrow set ESP
120
+ * matching can act on. Kept intentionally aligned with EMAIL_MAILBOX_PROVIDERS in
121
+ * tenant-db so a mailbox's `provider` and a resolved recipient ESP compare
122
+ * directly: we can only bias rotation toward a provider we can actually SEND
123
+ * from, and the pool is Google/Microsoft only.
124
+ *
125
+ * This is deliberately NARROWER than RECIPIENT_ESP_FAMILIES (what we DISPLAY).
126
+ * A domain behind Proofpoint has family "proofpoint" but may still be matchable
127
+ * as "microsoft" — see resolveEspFamily.
98
128
  */
99
129
  export declare const RECIPIENT_ESPS: readonly ["google", "microsoft"];
100
130
  export type RecipientEsp = (typeof RECIPIENT_ESPS)[number];
101
131
  /**
102
- * Infer a recipient's ESP from its email domain, DETERMINISTICALLY, for the
103
- * well-known consumer domains (gmail/googlemail → google; outlook/hotmail/live/
104
- * msn → microsoft). Returns null for any other domain — the caller then resolves
105
- * the ESP via a cached MX lookup (custom domains fronted by Google Workspace /
106
- * Microsoft 365 aren't decidable from the domain string alone). Pure and
107
- * side-effect-free so it's the single source of truth for the deterministic map.
132
+ * The DISPLAY taxonomy: the mail-infrastructure family a recipient domain sits
133
+ * on. Wider than RecipientEsp because "everything that isn't Google or Microsoft"
134
+ * is not one thing — a list that is 23% Proofpoint is a very different
135
+ * deliverability problem from one that is 23% self-hosted, and collapsing both to
136
+ * null (today's behavior) hides that. Split into three groups:
137
+ *
138
+ * - MAILBOX providers (google, microsoft) — imply a matchable ESP.
139
+ * - GATEWAY vendors (proofpoint, mimecast, ...) — security filters that sit IN
140
+ * FRONT of a mailbox provider. Their MX tells you the filter, NOT where the
141
+ * mailbox lives; the SPF fallback is what sees behind them.
142
+ * - OTHER mailbox hosts (zoho, yahoo, fastmail, ...) and self_hosted — real
143
+ * destinations we simply cannot send as, so never matchable.
144
+ */
145
+ export declare const RECIPIENT_ESP_FAMILIES: readonly ["google", "microsoft", "proofpoint", "mimecast", "barracuda", "cisco", "zscaler", "trendmicro", "sophos", "zoho", "yahoo", "apple", "proton", "fastmail", "gmx", "amazon", "self_hosted"];
146
+ export type RecipientEspFamily = (typeof RECIPIENT_ESP_FAMILIES)[number];
147
+ export declare function isRecipientEspFamily(value: unknown): value is RecipientEspFamily;
148
+ export declare function isEspGatewayFamily(family: RecipientEspFamily | null | undefined): boolean;
149
+ /**
150
+ * The matchable ESP a display family implies, or null when the family is a
151
+ * gateway (unknown until SPF) or a provider we cannot send as. The ONLY place
152
+ * family → esp is decided, so display and matching can't drift.
153
+ */
154
+ export declare function matchableEspFromFamily(family: RecipientEspFamily | null | undefined): RecipientEsp | null;
155
+ /**
156
+ * Map a domain's resolved MX exchange hostnames to a DISPLAY family. Returns null
157
+ * when no host matches any known vendor — the caller records that as
158
+ * "self_hosted" only once it is sure the lookup itself succeeded (an empty/failed
159
+ * answer is "unknown", which is a different fact).
160
+ */
161
+ export declare function familyFromMxHosts(hosts: readonly string[] | null | undefined): RecipientEspFamily | null;
162
+ /**
163
+ * Resolve the matchable ESP from a domain's TXT records by reading its SPF
164
+ * mechanisms. Scans every `v=spf1` record's include:/redirect= targets.
165
+ *
166
+ * AMBIGUITY IS NOT GUESSED: a domain mid-migration (or running Workspace
167
+ * alongside an Office tenant) can include BOTH vendors. Returning either one
168
+ * would be a coin flip that strict matching then acts on, so a both-match
169
+ * degrades to null ("unknown") and the caller falls back / defers honestly.
170
+ */
171
+ export declare function espFromSpfRecords(records: readonly string[] | null | undefined): RecipientEsp | null;
172
+ /**
173
+ * Infer a recipient's DISPLAY family from its domain string alone,
174
+ * DETERMINISTICALLY, for the well-known consumer/freemail domains. Returns null
175
+ * for any other domain — the caller then resolves via the cached MX (+ SPF)
176
+ * lookup, because a custom domain fronted by Workspace / M365 / a gateway is not
177
+ * decidable from the string. Pure: the single source of truth for the map.
178
+ */
179
+ export declare function inferRecipientEspFamily(domain: string | null | undefined): RecipientEspFamily | null;
180
+ /**
181
+ * Infer a recipient's MATCHABLE ESP from its domain string. Narrow by design:
182
+ * only the Google/Microsoft consumer domains resolve here, everything else
183
+ * (including yahoo/proton/icloud, which are real but unsendable-as) is null.
184
+ * Unchanged contract — the dispatcher's fast path still calls this.
108
185
  */
109
186
  export declare function inferRecipientEsp(domain: string | null | undefined): RecipientEsp | null;
110
187
  /**
111
- * Map a domain's resolved MX exchange hostnames to an ESP family, for the MX-cache
112
- * path (custom domains on Google Workspace / Microsoft 365). Google MX hosts end
113
- * in `google.com` / `googlemail.com` (e.g. aspmx.l.google.com); Microsoft 365 MX
114
- * hosts end in `outlook.com` / `protection.outlook.com` (e.g.
115
- * acme-com.mail.protection.outlook.com). Returns null when no host matches either
116
- * family. Pure so both the resolver and its tests share one mapping.
188
+ * Back-compat narrow view of familyFromMxHosts: MX hosts → matchable ESP. Now
189
+ * derived from the family table so the two can't drift, and gateway MX hosts
190
+ * correctly yield null here (they need the SPF signal to become matchable).
117
191
  */
118
192
  export declare function espFromMxHosts(hosts: readonly string[] | null | undefined): RecipientEsp | null;
119
193
  /**
@@ -142,6 +216,35 @@ export declare function whatsAppAttendeeIdForPhone(phone: string): string | null
142
216
  * enroll/plan path and any preview count can't drift.
143
217
  */
144
218
  export declare function whatsAppAttendeeIdFromRow(rowValues: Record<string, unknown> | null | undefined, phoneColumnKey?: string | null): string | null;
219
+ /**
220
+ * Normalize a raw phone number to strict E.164 (`+` then 2-15 digits, no leading
221
+ * zero) for the CALL channel. Deliberately stricter than
222
+ * whatsAppAttendeeIdForPhone, which accepts any 7-15 bare digits: on WhatsApp a
223
+ * bad guess fails an API call, but on the phone it rings a real stranger.
224
+ *
225
+ * Rules — anything ambiguous returns null rather than guessing:
226
+ * - already "+…" -> strip separators, validate, keep.
227
+ * - 10 bare digits -> NANP national number, prefix "+1".
228
+ * - 11 bare digits starting with "1" -> NANP with country code, prefix "+".
229
+ * - anything else -> null.
230
+ *
231
+ * SCOPE: bare (non-"+") input is completed ONLY for the NANP, where the national
232
+ * number is always exactly 10 digits. Every other country has a variable national
233
+ * length (UK 10, Germany 6-11, …), so a bare string genuinely cannot be resolved
234
+ * to one number — those callers must supply full E.164. That matches v1, which
235
+ * provisions US/Canada numbers only.
236
+ *
237
+ * When international dialing lands, replace this with libphonenumber-js rather
238
+ * than extending the heuristic — per-country national-number rules are a lookup
239
+ * table, not something to hand-roll against live prospects.
240
+ */
241
+ export declare function callPhoneForLead(phone: string): string | null;
242
+ /**
243
+ * Resolve a lead's dialable E.164 number from its row_values, reusing the SAME
244
+ * column precedence WhatsApp uses (SEQUENCE_PHONE_COLUMN_KEYS) so the two phone
245
+ * channels can never disagree about which column holds the number.
246
+ */
247
+ export declare function callPhoneFromRow(rowValues: Record<string, unknown> | null | undefined, phoneColumnKey?: string | null): string | null;
145
248
  /**
146
249
  * A per-step / per-sequence send window. `days` are ISO weekdays (1=Mon … 7=Sun)
147
250
  * on which sends are allowed; `start`/`end` are "HH:MM" local times. The window
@@ -215,7 +318,7 @@ export type SequenceAutoOptimizeConfig = {
215
318
  min_sends_per_variant: number;
216
319
  min_conversions: number;
217
320
  };
218
- export declare const SEQUENCE_STEP_KINDS: readonly ["visit_profile", "invite", "wait_for_connection", "message", "inmail", "follow", "like_post", "comment_post", "withdraw_invite", "email_send", "email_reply", "email_enroll", "email_move", "email_stop", "whatsapp_message", "wait", "wait_for_signal", "branch", "stop"];
321
+ export declare const SEQUENCE_STEP_KINDS: readonly ["visit_profile", "invite", "wait_for_connection", "message", "inmail", "follow", "like_post", "comment_post", "withdraw_invite", "email_send", "email_reply", "email_enroll", "email_move", "email_stop", "whatsapp_message", "call_task", "crm_task", "wait", "wait_for_signal", "branch", "stop"];
219
322
  export type SequenceStepKind = (typeof SEQUENCE_STEP_KINDS)[number];
220
323
  export type SequenceLinkedInVisitProfileStep = {
221
324
  id: string;
@@ -384,6 +487,137 @@ export type SequenceWhatsAppMessageStep = {
384
487
  /** Opt-in reversible auto-winner over the variants (metric: reply | click | open; reply recommended). */
385
488
  auto_optimize?: SequenceAutoOptimizeConfig;
386
489
  };
490
+ /**
491
+ * Queue a call for a human. The one step kind Oxygen does not execute itself.
492
+ *
493
+ * Planning it inserts an ox_sequencer.call_tasks row and parks the enrollment
494
+ * awaiting a call signal, exactly like wait_for_signal — dispositioning the call
495
+ * writes call_connected / call_no_answer / call_voicemail, which resumes it. That
496
+ * is why `timeout_days` and `on_timeout` mirror the wait_for_signal shape: a lead
497
+ * nobody ever calls must not sit in a sequence forever.
498
+ *
499
+ * `note` is talking points shown to the rep on the dial screen, NOT a message —
500
+ * it is never sent anywhere, so it is optional and a blank one does not block a
501
+ * launch the way empty message copy does.
502
+ */
503
+ export type SequenceCallTaskStep = {
504
+ id: string;
505
+ channel: "call";
506
+ kind: "call_task";
507
+ /** Talking points for the rep. Supports {{column}}. Never transmitted. */
508
+ note?: string;
509
+ /** Queue ordering; higher dials first. Defaults to 0. */
510
+ priority?: number;
511
+ /** Days to wait for a disposition before acting on on_timeout. */
512
+ timeout_days: number;
513
+ /** "stop" ends the sequence if nobody calls; "continue" advances. Defaults to "continue". */
514
+ on_timeout: "stop" | "continue";
515
+ };
516
+ export declare const SEQUENCE_CRM_TASK_PROVIDERS: readonly ["hubspot"];
517
+ export type SequenceCrmTaskProvider = (typeof SEQUENCE_CRM_TASK_PROVIDERS)[number];
518
+ /**
519
+ * Provider-property value mapping for a CRM task. Keeping the source shape
520
+ * provider-neutral means a future Salesforce/Pipedrive adapter can consume the
521
+ * exact same sequence definition while mapping different destination property
522
+ * names.
523
+ */
524
+ export type SequenceCrmTaskValueMapping = {
525
+ source: "literal";
526
+ value: string | number | boolean;
527
+ } | {
528
+ source: "row";
529
+ key: string;
530
+ } | {
531
+ source: "template";
532
+ template: string;
533
+ } | {
534
+ source: "relative_time";
535
+ offset_minutes: number;
536
+ };
537
+ export declare const SEQUENCE_CRM_IDENTITY_KINDS: readonly ["provider_id", "email", "domain", "linkedin_url", "exact"];
538
+ export type SequenceCrmIdentityKind = (typeof SEQUENCE_CRM_IDENTITY_KINDS)[number];
539
+ /**
540
+ * One exact identity used to resolve an existing provider record. `kind`
541
+ * controls normalization while `property` remains provider-configurable (for
542
+ * example HubSpot's `email` or a customer's custom LinkedIn URL property).
543
+ */
544
+ export type SequenceCrmRecordIdentityMapping = {
545
+ kind: SequenceCrmIdentityKind;
546
+ /** Destination property internal name. Omitted only for provider_id. */
547
+ property?: string;
548
+ value: SequenceCrmTaskValueMapping;
549
+ };
550
+ /**
551
+ * A named person/account/deal binding resolved before the task is created.
552
+ * Names are local sequence references; object/property names are deliberately
553
+ * dynamic so another CRM adapter can consume the same lifecycle.
554
+ */
555
+ export type SequenceCrmRecordMapping = {
556
+ /** Destination object type, e.g. contacts, companies, people, or deals. */
557
+ object_type: string;
558
+ /** Ordered exact identities. Name-only matching is intentionally unsupported. */
559
+ identities: SequenceCrmRecordIdentityMapping[];
560
+ /** Dynamic provider properties applied on create/update. */
561
+ property_mappings: Record<string, SequenceCrmTaskValueMapping>;
562
+ /** Missing records are only created when explicitly authorized. */
563
+ on_missing: "fail" | "create";
564
+ /** Existing records are reused by default; update must be explicit. */
565
+ on_match: "reuse" | "update";
566
+ };
567
+ export type SequenceCrmRecordLink = {
568
+ from_record: string;
569
+ to_record: string;
570
+ /**
571
+ * Optional provider association type. HubSpot's default relation is used when
572
+ * absent; custom labels/types can be supplied without changing the DSL.
573
+ */
574
+ association_type_id?: number;
575
+ association_category?: "HUBSPOT_DEFINED" | "USER_DEFINED";
576
+ };
577
+ type SequenceCrmTaskAssociationOptions = {
578
+ association_type_id?: number;
579
+ association_category?: "HUBSPOT_DEFINED" | "USER_DEFINED";
580
+ };
581
+ export type SequenceCrmTaskAssociation = SequenceCrmTaskAssociationOptions & ({
582
+ /** Attach the task to a record resolved by record_mappings. */
583
+ record_ref: string;
584
+ /** Optional override; otherwise the referenced record's object_type wins. */
585
+ object_type?: string;
586
+ object_id?: never;
587
+ } | {
588
+ /** Destination CRM object type, e.g. contacts, companies, or deals. */
589
+ object_type: string;
590
+ /** Provider record id resolved from a literal, source-row field, or template. */
591
+ object_id: SequenceCrmTaskValueMapping;
592
+ record_ref?: never;
593
+ });
594
+ /**
595
+ * Create one task in a connected CRM and advance after the provider confirms
596
+ * creation. Unlike call_task this is an external write, not a human wait gate.
597
+ */
598
+ export type SequenceCrmTaskStep = {
599
+ id: string;
600
+ channel: "crm";
601
+ kind: "crm_task";
602
+ provider: SequenceCrmTaskProvider;
603
+ /** Optional explicit connection; absent resolves the workspace's active one. */
604
+ connection_id?: string;
605
+ /**
606
+ * Destination property internal-name → value source. Property names remain
607
+ * dynamic so custom CRM fields do not require a new Oxygen release.
608
+ */
609
+ property_mappings: Record<string, SequenceCrmTaskValueMapping>;
610
+ /**
611
+ * Optional named provider records to resolve/upsert before creating the task.
612
+ * Exact identity matches are reused, conflicting identities fail closed, and
613
+ * provider writes are checkpointed independently by the runtime.
614
+ */
615
+ record_mappings?: Record<string, SequenceCrmRecordMapping>;
616
+ /** Optional links between named records, e.g. person → account. */
617
+ record_links?: SequenceCrmRecordLink[];
618
+ /** Optional records to associate the newly-created task with. */
619
+ associations?: SequenceCrmTaskAssociation[];
620
+ };
387
621
  export type SequenceWaitStep = {
388
622
  id: string;
389
623
  kind: "wait";
@@ -501,7 +735,7 @@ export type SequenceSignalCondition = {
501
735
  } | {
502
736
  not: SequenceSignalCondition;
503
737
  };
504
- export type SequenceStep = SequenceLinkedInVisitProfileStep | SequenceLinkedInInviteStep | SequenceLinkedInWaitForConnectionStep | SequenceLinkedInMessageStep | SequenceLinkedInInMailStep | SequenceLinkedInFollowStep | SequenceLinkedInLikePostStep | SequenceLinkedInCommentPostStep | SequenceLinkedInWithdrawInviteStep | SequenceEmailSendStep | SequenceEmailReplyStep | SequenceEmailEnrollStep | SequenceEmailMoveStep | SequenceEmailStopStep | SequenceWhatsAppMessageStep | SequenceWaitStep | SequenceWaitForSignalStep | SequenceBranchStep | SequenceStopStep;
738
+ export type SequenceStep = SequenceLinkedInVisitProfileStep | SequenceLinkedInInviteStep | SequenceLinkedInWaitForConnectionStep | SequenceLinkedInMessageStep | SequenceLinkedInInMailStep | SequenceLinkedInFollowStep | SequenceLinkedInLikePostStep | SequenceLinkedInCommentPostStep | SequenceLinkedInWithdrawInviteStep | SequenceEmailSendStep | SequenceEmailReplyStep | SequenceEmailEnrollStep | SequenceEmailMoveStep | SequenceEmailStopStep | SequenceWhatsAppMessageStep | SequenceCallTaskStep | SequenceCrmTaskStep | SequenceWaitStep | SequenceWaitForSignalStep | SequenceBranchStep | SequenceStopStep;
505
739
  export type SequenceDefinition = {
506
740
  steps: SequenceStep[];
507
741
  };
@@ -526,7 +760,35 @@ export type ValidateSequenceOptions = {
526
760
  * the gate would silently dead-end).
527
761
  */
528
762
  nativeTrackingEnabled?: boolean;
763
+ /**
764
+ * This definition is a DRAFT being authored, so permit an incomplete authoring
765
+ * state: a sequence with no steps yet, and steps whose copy has not been
766
+ * written. Default false — a launch-ready definition must have both.
767
+ *
768
+ * The web builder autosaves on every edit, so it necessarily persists states no
769
+ * launch-ready sequence may have: the moment you delete the last step, or drop a
770
+ * "Send message" before typing it. Rejecting those wedged autosave — the
771
+ * definition could not be stored at all, so the edit was lost on navigate.
772
+ *
773
+ * Completeness is a LAUNCH concern, not a storage one. `sequenceStepsMissingCopy`
774
+ * and an empty-steps check are the gates the start route runs before a sequence
775
+ * may go active, and draft -> active is the only route to sending (the status
776
+ * route refuses that transition outright).
777
+ */
778
+ draft?: boolean;
529
779
  };
780
+ /** One step whose copy is still empty, for the launch-readiness blocker. */
781
+ export type SequenceMissingCopy = {
782
+ stepId: string;
783
+ kind: SequenceStepKind;
784
+ field: string;
785
+ };
786
+ /**
787
+ * Steps that still have empty copy — the launch gate for definitions stored
788
+ * stored under `draft`. Empty means nothing to send, so starting would
789
+ * either dispatch a blank message or fail per lead at send time.
790
+ */
791
+ export declare function sequenceStepsMissingCopy(steps: readonly SequenceStep[]): SequenceMissingCopy[];
530
792
  /**
531
793
  * Validate + normalize a raw sequence definition. Assigns stable ids to steps
532
794
  * that omit one (s{index}), verifies branch/gate targets resolve, and enforces
@@ -556,7 +818,8 @@ export declare function sequenceWaitStepDelayWithJitterMs(step: SequenceWaitStep
556
818
  /**
557
819
  * Render a sequence-copy template against a row's values. Delegates to the shared
558
820
  * deterministic engine (sequence-template.ts): `{{column}}` substitution plus
559
- * `{{column|fallback}}`, `{{RANDOM|…}}` spintax, and `{% if … %}` conditionals.
821
+ * `{{column|fallback}}`, `{{RANDOM|…}}` and bare `{a|b|c}` spintax (nestable), and
822
+ * `{% if … %}` conditionals.
560
823
  * Pass `{ seed }` to make spintax choices replayable across retries/crash-replays.
561
824
  */
562
825
  export declare function renderSequenceTemplate(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string;