@oxygen-agent/cli 1.233.8 → 1.244.2

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.
@@ -58,6 +58,19 @@ export const SEQUENCE_SIGNALS = [
58
58
  "web_visit",
59
59
  "intent",
60
60
  ];
61
+ /**
62
+ * Email engagement signals that ONLY arrive via the Instantly webhook
63
+ * (email_opened/email_clicked). Native email sends (Gmail API / Microsoft Graph)
64
+ * produce no open/click tracking, so a sequence that sends email natively can
65
+ * never accumulate these — gating a wait_for_signal or signal-branch on them
66
+ * silently always times out / always takes the else arm. validateSequenceDefinition
67
+ * rejects that misconfiguration. (email_replied/email_bounced DO arrive natively
68
+ * via the inbound-message + bounce paths, so they are not listed here.)
69
+ */
70
+ export const SEQUENCE_NATIVE_UNTRACKED_SIGNALS = ["email_opened", "email_clicked"];
71
+ function isNativeUntrackedSignal(value) {
72
+ return SEQUENCE_NATIVE_UNTRACKED_SIGNALS.includes(value);
73
+ }
61
74
  /** External GTM signals (the non-engagement subset of SEQUENCE_SIGNALS). */
62
75
  export const SEQUENCE_EXTERNAL_SIGNALS = [
63
76
  "company_hiring",
@@ -228,6 +241,14 @@ export function validateSequenceDefinition(input, options = {}) {
228
241
  }
229
242
  }
230
243
  });
244
+ // Pass 3: native-email engagement-gate guard. Native email sends produce no
245
+ // open/click signals (email_opened/email_clicked arrive only from the Instantly
246
+ // webhook). A sequence that sends email natively — has native email steps
247
+ // (email_send/email_reply) and is NOT delegated to an Instantly campaign (no
248
+ // email_enroll/email_move/email_stop steps) — that gates a wait_for_signal or
249
+ // signal-branch on those signals would silently always time out / always take
250
+ // the else arm. Reject it at write time so create/update report the dead gate.
251
+ reportNativeEngagementGates(normalized, issues);
231
252
  if (issues.length > 0) {
232
253
  throw new OxygenError("invalid_sequence", `Sequence definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`, { details: { issues }, exitCode: 1 });
233
254
  }
@@ -248,6 +269,52 @@ export function lintSequenceDefinition(input, options = {}) {
248
269
  return [{ path: "steps", message: error instanceof Error ? error.message : "Invalid sequence." }];
249
270
  }
250
271
  }
272
+ /**
273
+ * Push an issue for every wait_for_signal / signal-branch that gates on a
274
+ * natively-untracked engagement signal (email_opened/email_clicked) inside a
275
+ * sequence that sends email natively. "Native email" = at least one
276
+ * email_send/email_reply step and NO Instantly-delegated email step
277
+ * (email_enroll/email_move/email_stop); an Instantly-bound sequence DOES receive
278
+ * those webhook signals, so it is left alone. Mutates `issues` in place.
279
+ */
280
+ function reportNativeEngagementGates(steps, issues) {
281
+ const hasNativeEmail = steps.some((s) => s.kind === "email_send" || s.kind === "email_reply");
282
+ const hasInstantlyEmail = steps.some((s) => s.kind === "email_enroll" || s.kind === "email_move" || s.kind === "email_stop");
283
+ if (!hasNativeEmail || hasInstantlyEmail)
284
+ return;
285
+ steps.forEach((step, index) => {
286
+ if (step.kind === "wait_for_signal" && isNativeUntrackedSignal(step.signal)) {
287
+ issues.push({
288
+ path: `steps[${index}].signal`,
289
+ message: `step '${step.id}' waits for '${step.signal}', but this sequence sends email natively (no Instantly binding), which produces no open/click signals — the gate would always time out. Use email_replied/email_bounced, or bind an Instantly campaign to track opens/clicks.`,
290
+ });
291
+ return;
292
+ }
293
+ if (step.kind === "branch" && typeof step.condition !== "string") {
294
+ const referenced = new Set();
295
+ collectConditionSignals(step.condition, referenced);
296
+ for (const signal of SEQUENCE_NATIVE_UNTRACKED_SIGNALS) {
297
+ if (referenced.has(signal)) {
298
+ issues.push({
299
+ path: `steps[${index}].condition`,
300
+ message: `step '${step.id}' branches on '${signal}', but this sequence sends email natively (no Instantly binding), which produces no open/click signals — the branch would always take the else arm. Use email_replied/email_bounced, or bind an Instantly campaign to track opens/clicks.`,
301
+ });
302
+ }
303
+ }
304
+ }
305
+ });
306
+ }
307
+ /** Collect every signal name referenced anywhere in a signal-branch condition. */
308
+ function collectConditionSignals(condition, out) {
309
+ if ("signal" in condition)
310
+ out.add(condition.signal);
311
+ else if ("all" in condition)
312
+ condition.all.forEach((c) => collectConditionSignals(c, out));
313
+ else if ("any" in condition)
314
+ condition.any.forEach((c) => collectConditionSignals(c, out));
315
+ else
316
+ collectConditionSignals(condition.not, out);
317
+ }
251
318
  function collectSteps(input, issues) {
252
319
  const record = isRecord(input) ? input : null;
253
320
  const steps = record?.steps;
@@ -774,18 +841,13 @@ function normalizeVariants(raw, path, fields, issues) {
774
841
  });
775
842
  return out.length > 0 ? out : undefined;
776
843
  }
777
- function normalizeSendWindow(raw, path, issues) {
778
- if (raw === undefined || raw === null)
779
- return undefined;
780
- if (!isRecord(raw)) {
781
- issues.push({ path, message: "send_window must be an object." });
782
- return undefined;
783
- }
844
+ function normalizeSendWindowConfiguredTimezone(raw, path, issues) {
784
845
  const timezone = typeof raw.timezone === "string" && raw.timezone.trim() ? raw.timezone.trim() : undefined;
785
846
  if (!timezone)
786
847
  issues.push({ path: `${path}.timezone`, message: "timezone (IANA, e.g. America/New_York) is required." });
787
- const start = normalizeHhmm(raw.start, `${path}.start`, issues);
788
- const end = normalizeHhmm(raw.end, `${path}.end`, issues);
848
+ return timezone ?? "UTC";
849
+ }
850
+ function validateSendWindowBounds(start, end, path, issues) {
789
851
  // A zero-width window (start === end) is never open — isWithinSendWindow
790
852
  // returns false for every instant — so a step carrying it defers
791
853
  // (outside_send_window) on every dispatch and the enrollment silently stalls
@@ -794,7 +856,8 @@ function normalizeSendWindow(raw, path, issues) {
794
856
  if (start !== undefined && end !== undefined && hhmmToMinutes(start) === hhmmToMinutes(end)) {
795
857
  issues.push({ path: `${path}.end`, message: "must differ from start — a zero-width send_window never sends." });
796
858
  }
797
- const days = normalizeWeekdays(raw.days, `${path}.days`, issues);
859
+ }
860
+ function normalizeSendWindowTimezoneMode(raw, path, issues) {
798
861
  const mode = raw.timezone_mode === "recipient" ? "recipient" : "fixed";
799
862
  const column = mode === "recipient"
800
863
  ? optionalString(raw.recipient_timezone_column, `${path}.recipient_timezone_column`, issues)
@@ -802,8 +865,23 @@ function normalizeSendWindow(raw, path, issues) {
802
865
  if (mode === "recipient" && !column) {
803
866
  issues.push({ path: `${path}.recipient_timezone_column`, message: 'recipient_timezone_column is required when timezone_mode is "recipient".' });
804
867
  }
868
+ return { mode, column };
869
+ }
870
+ function normalizeSendWindow(raw, path, issues) {
871
+ if (raw === undefined || raw === null)
872
+ return undefined;
873
+ if (!isRecord(raw)) {
874
+ issues.push({ path, message: "send_window must be an object." });
875
+ return undefined;
876
+ }
877
+ const timezone = normalizeSendWindowConfiguredTimezone(raw, path, issues);
878
+ const start = normalizeHhmm(raw.start, `${path}.start`, issues);
879
+ const end = normalizeHhmm(raw.end, `${path}.end`, issues);
880
+ validateSendWindowBounds(start, end, path, issues);
881
+ const days = normalizeWeekdays(raw.days, `${path}.days`, issues);
882
+ const { mode, column } = normalizeSendWindowTimezoneMode(raw, path, issues);
805
883
  return {
806
- timezone: timezone ?? "UTC",
884
+ timezone,
807
885
  ...(days ? { days } : {}),
808
886
  start: start ?? "09:00",
809
887
  end: end ?? "17:00",
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.233.8";
1
+ export declare const OXYGEN_VERSION = "1.244.2";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.233.8";
1
+ export const OXYGEN_VERSION = "1.244.2";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  // 1.181.0: paid table action runs and background columns run require
4
4
  // approved=true in addition to max_credits; older CLIs cannot send the flag.
@@ -15,6 +15,11 @@
15
15
  "types": "./dist/file-import.d.ts",
16
16
  "import": "./dist/file-import.js",
17
17
  "default": "./dist/file-import.js"
18
+ },
19
+ "./custom-http-safety": {
20
+ "types": "./dist/custom-http-safety.d.ts",
21
+ "import": "./dist/custom-http-safety.js",
22
+ "default": "./dist/custom-http-safety.js"
18
23
  }
19
24
  },
20
25
  "dependencies": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.233.8",
3
+ "version": "1.244.2",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",