@gravity-rail/sdk 0.15.0 → 0.17.0

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 (42) hide show
  1. package/dist/agui.cjs +13 -1
  2. package/dist/agui.cjs.map +1 -1
  3. package/dist/agui.d.cts +5 -1
  4. package/dist/agui.d.ts +5 -1
  5. package/dist/agui.js +12 -1
  6. package/dist/agui.js.map +1 -1
  7. package/dist/{chunk-QTJQTTIR.js → chunk-CEFOCGYO.js} +521 -69
  8. package/dist/chunk-CEFOCGYO.js.map +1 -0
  9. package/dist/{chunk-GP6ICMWF.js → chunk-PJFEGUHM.js} +8 -1
  10. package/dist/chunk-PJFEGUHM.js.map +1 -0
  11. package/dist/{client-BLuz00Qr.d.cts → client-8xakCsVs.d.cts} +2245 -232
  12. package/dist/{client-CefA5Mxe.d.ts → client-ASnNEN1U.d.ts} +2245 -232
  13. package/dist/helpers.cjs +7 -0
  14. package/dist/helpers.cjs.map +1 -1
  15. package/dist/helpers.d.cts +2 -2
  16. package/dist/helpers.d.ts +2 -2
  17. package/dist/helpers.js +1 -1
  18. package/dist/index.cjs +898 -87
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +182 -9
  21. package/dist/index.d.ts +182 -9
  22. package/dist/index.js +397 -23
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal.cjs +607 -68
  25. package/dist/internal.cjs.map +1 -1
  26. package/dist/internal.d.cts +328 -7
  27. package/dist/internal.d.ts +328 -7
  28. package/dist/internal.js +86 -4
  29. package/dist/internal.js.map +1 -1
  30. package/dist/{revision-state-axZ62kJk.d.cts → revision-state-BDsQo_xD.d.cts} +4 -2
  31. package/dist/{revision-state-DL1ViZ57.d.ts → revision-state-Cz3mcUHc.d.ts} +4 -2
  32. package/dist/schemas.cjs +179 -217
  33. package/dist/schemas.cjs.map +1 -1
  34. package/dist/schemas.d.cts +2 -2
  35. package/dist/schemas.d.ts +2 -2
  36. package/dist/schemas.js +179 -210
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/{tasks-Boi6AxGp.d.cts → tasks-Z4GcIqgO.d.cts} +200 -81
  39. package/dist/{tasks-Boi6AxGp.d.ts → tasks-Z4GcIqgO.d.ts} +200 -81
  40. package/package.json +3 -3
  41. package/dist/chunk-GP6ICMWF.js.map +0 -1
  42. package/dist/chunk-QTJQTTIR.js.map +0 -1
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  MemberImportValidationError,
45
45
  MemberType,
46
46
  MenuItemResponseItemType,
47
+ MergeMemberRolesRequestAccessStrategy,
47
48
  ModelProvider,
48
49
  ModelType,
49
50
  NO_ASSISTANT_PARTICIPANT_CHAT_TYPES,
@@ -65,6 +66,7 @@ import {
65
66
  PIDS_SOURCES,
66
67
  POSITIVE_RATING_THRESHOLD,
67
68
  ProductType,
69
+ PromoCodeStatus,
68
70
  QUERY_VALUELESS_OPS,
69
71
  QueriesApi,
70
72
  REALTIME_MODEL_CUSTOM,
@@ -100,9 +102,12 @@ import {
100
102
  WIDGET_SLUGS,
101
103
  WORKFLOW_IMPLICIT_USE_SCOPE,
102
104
  WorkflowAccessWarningResourceType,
105
+ WorkflowRoutingMode,
103
106
  WorkspaceFeature,
104
107
  WorkspaceProvisionStatus,
108
+ archivedMemberConflictId,
105
109
  areDependenciesSatisfied,
110
+ asIdentifiedMemberContactConflict,
106
111
  canPreviewVoice,
107
112
  clearRegistry,
108
113
  createWorkspaceClient,
@@ -125,9 +130,12 @@ import {
125
130
  getTTSProvider,
126
131
  getVoiceModelId,
127
132
  initializeFeatures,
133
+ isAccountContactConflict,
128
134
  isAgentNoteOrigin,
129
135
  isChatType,
130
136
  isFeatureAvailable,
137
+ isIdentifiedMemberContactConflict,
138
+ isMemberContactConflict,
131
139
  isPositiveRating,
132
140
  isRegistryInitialized,
133
141
  isSMSDeliveryFailureStatus,
@@ -137,7 +145,7 @@ import {
137
145
  roleWorkflowAccess,
138
146
  usesBuiltinAssistant,
139
147
  validateFeatureDependencies
140
- } from "./chunk-QTJQTTIR.js";
148
+ } from "./chunk-CEFOCGYO.js";
141
149
  import {
142
150
  AttendeeResponseStatus,
143
151
  AttendeeStatus,
@@ -158,7 +166,7 @@ import {
158
166
  BaseApiClient,
159
167
  getCookieValue,
160
168
  parseOrgAuthChallenge
161
- } from "./chunk-GP6ICMWF.js";
169
+ } from "./chunk-PJFEGUHM.js";
162
170
 
163
171
  // src/workspace-builder/resource.ts
164
172
  function isRecord(value) {
@@ -195,6 +203,14 @@ var WorkspaceBuilderResource = class {
195
203
  }
196
204
  return this._uuid;
197
205
  }
206
+ /**
207
+ * True for resources the server creates on our behalf (currently a
208
+ * journey's `Start` step). They are kept in the resource list so edges can
209
+ * reference them, but they are never created, printed, or counted.
210
+ */
211
+ get isImplicit() {
212
+ return false;
213
+ }
198
214
  matchKey() {
199
215
  return void 0;
200
216
  }
@@ -217,6 +233,19 @@ function resolveRefValue(value) {
217
233
  }
218
234
  return out;
219
235
  }
236
+ function resolveIdRefs(value) {
237
+ return resolveIdRefValue(value);
238
+ }
239
+ function resolveIdRefValue(value) {
240
+ if (value instanceof WorkspaceBuilderResource) return value.id;
241
+ if (Array.isArray(value)) return value.map(resolveIdRefValue);
242
+ if (!isPlainRecord(value)) return value;
243
+ const out = {};
244
+ for (const [key, nestedValue] of Object.entries(value)) {
245
+ out[key] = resolveIdRefValue(nestedValue);
246
+ }
247
+ return out;
248
+ }
220
249
 
221
250
  // src/workspace-builder/automation.ts
222
251
  var MemberFilterBuilder = class extends WorkspaceBuilderResource {
@@ -230,7 +259,12 @@ var MemberFilterBuilder = class extends WorkspaceBuilderResource {
230
259
  description;
231
260
  /** Query-engine predicate (JSON IR) authored on MemberFilter.query. */
232
261
  query = null;
233
- /** Set the filter's query-engine predicate (JSON IR). */
262
+ /**
263
+ * Set the filter's query-engine predicate (JSON IR).
264
+ *
265
+ * Anywhere the IR wants a numeric id you may pass the builder that will own
266
+ * it (e.g. a label in a `member.labelId` value); refs resolve at apply time.
267
+ */
234
268
  setQuery(query) {
235
269
  this.query = query;
236
270
  return this;
@@ -239,10 +273,12 @@ var MemberFilterBuilder = class extends WorkspaceBuilderResource {
239
273
  return `MemberFilter: ${this.name}${this.query ? " (query)" : ""}`;
240
274
  }
241
275
  async apply(client) {
276
+ if (this._existed) return;
242
277
  const payload = {
243
278
  name: this.name,
244
279
  description: this.description,
245
- query: this.query
280
+ // Builder refs inside the IR become ids here; see resolveIdRefs.
281
+ query: resolveIdRefs(this.query)
246
282
  };
247
283
  const result = await client.createMemberFilter(payload);
248
284
  this._id = result.id;
@@ -537,35 +573,134 @@ var MemberLabelBuilder = class extends WorkspaceBuilderResource {
537
573
  };
538
574
 
539
575
  // src/workspace-builder/journeys.ts
576
+ var SECONDS_PER_DAY = 86400;
577
+ function edgeKey(parentId, childId) {
578
+ return `${parentId}:${childId}`;
579
+ }
580
+ function edgeCondition(options) {
581
+ const clauses = [];
582
+ if (options.afterDays !== void 0) {
583
+ clauses.push(`days_since(parent_step.completed_at) >= ${options.afterDays}`);
584
+ }
585
+ if (options.when) clauses.push(`(${options.when})`);
586
+ if (!clauses.length) return void 0;
587
+ return clauses.join(" && ");
588
+ }
540
589
  var JourneyStepBuilder = class extends WorkspaceBuilderResource {
541
590
  // journey-decoupling Phase 7: a Journey step no longer links to a Workflow.
542
591
  // A step nudges via EventRule actions instead of running a Workflow, so the
543
592
  // builder no longer accepts or emits a workflow reference.
544
- constructor(name, journey, order = 0, description = "") {
593
+ constructor(name, journey, order = 0, description = "", options = {}) {
545
594
  super();
546
595
  this.name = name;
547
596
  this.journey = journey;
548
597
  this.order = order;
549
598
  this.description = description;
599
+ this.assigneeId = options.assigneeId;
600
+ this.posX = options.posX;
601
+ this.posY = options.posY;
550
602
  }
551
603
  name;
552
604
  journey;
553
605
  order;
554
606
  description;
555
607
  connections = [];
556
- connectsTo(child, condition, required = false, timeoutSeconds) {
557
- this.connections.push({ parent: this, child, condition, required, timeoutSeconds });
608
+ rules = [];
609
+ assigneeId;
610
+ posX;
611
+ posY;
612
+ /** Wire an edge to `child` and return the CHILD, so chains read linearly. */
613
+ connectsTo(child, options = {}) {
614
+ if (child === this) {
615
+ throw new Error(`Journey step '${this.name}' cannot connect to itself`);
616
+ }
617
+ const abandonSeconds = options.abandonAfterDays !== void 0 ? options.abandonAfterDays * SECONDS_PER_DAY : options.timeoutSeconds;
618
+ this.connections.push({
619
+ parent: this,
620
+ child,
621
+ condition: edgeCondition(options),
622
+ required: options.required ?? false,
623
+ timeoutSeconds: abandonSeconds,
624
+ order: options.order,
625
+ filterId: options.filterId
626
+ });
627
+ return child;
628
+ }
629
+ /**
630
+ * Attach a `JOURNEY_STEP_EXECUTE` action — what this step actually DOES
631
+ * when a member reaches it. A step with no rules is inert: the member
632
+ * lands on it, nothing is sent, and the journey moves on.
633
+ */
634
+ onExecute(actionType, params = {}, options = {}) {
635
+ this.rules.push({
636
+ uuid: crypto.randomUUID(),
637
+ actionType,
638
+ actionParams: params,
639
+ options
640
+ });
558
641
  return this;
559
642
  }
643
+ /**
644
+ * Text the member. `phoneNumberUUID` is a workspace PhoneNumber with SMS
645
+ * enabled. Pass `workflowUUID` to start an SMS workflow chat, or
646
+ * `messageTemplate` for a one-off send.
647
+ */
648
+ sendsSms(params, options = {}) {
649
+ return this.onExecute("sms:send" /* SMS_SEND */, params, options);
650
+ }
651
+ /**
652
+ * Call the member. Both the workspace PhoneNumber uuid (voice enabled) and
653
+ * the Workflow uuid that drives the call are required.
654
+ */
655
+ callsMember(params, options = {}) {
656
+ return this.onExecute("phone:call" /* PHONE_CALL */, params, options);
657
+ }
658
+ /** Email the member from a workspace inbox. */
659
+ sendsEmail(params, options = {}) {
660
+ return this.onExecute("email:send" /* EMAIL_SEND */, params, options);
661
+ }
662
+ /** Label the member on arrival — pass label builders, not ids. */
663
+ addsLabels(labels, options = {}) {
664
+ return this.onExecute(
665
+ "member:label" /* MEMBER_LABEL_ACTION */,
666
+ { addLabelIds: labels },
667
+ options
668
+ );
669
+ }
670
+ /** True once this step has something to do when a member reaches it. */
671
+ get hasOutreach() {
672
+ return this.rules.length > 0;
673
+ }
674
+ eventRuleInputs() {
675
+ return this.rules.map((rule) => ({
676
+ uuid: rule.uuid,
677
+ eventType: "journey_step:execute" /* JOURNEY_STEP_EXECUTE */,
678
+ actionType: rule.actionType,
679
+ actionParams: resolveRefs(rule.actionParams),
680
+ ...rule.options.name ? { name: rule.options.name } : {},
681
+ ...rule.options.when ? { conditions: rule.options.when } : {},
682
+ ...rule.options.delaySeconds ? { delay: rule.options.delaySeconds } : {},
683
+ active: rule.options.active ?? true
684
+ }));
685
+ }
560
686
  planLine() {
561
- return `Step: ${this.name}`;
687
+ const actions = this.rules.map((rule) => rule.actionType).join(" + ");
688
+ return `Step: ${this.name}${actions ? ` (${actions})` : ""}`;
562
689
  }
563
690
  async apply(client) {
691
+ if (this._existed) return;
564
692
  if (this.journey._id === void 0) return;
693
+ const rules = this.eventRuleInputs();
565
694
  const payload = {
566
695
  name: this.name,
567
696
  description: this.description,
568
- order: this.order
697
+ order: this.order,
698
+ ...this.assigneeId !== void 0 ? { assigneeId: this.assigneeId } : {},
699
+ ...this.posX !== void 0 ? { posX: this.posX } : {},
700
+ ...this.posY !== void 0 ? { posY: this.posY } : {},
701
+ // Saved atomically with the step, so a partial failure cannot leave
702
+ // an inert step behind.
703
+ ...rules.length ? { eventRules: rules } : {}
569
704
  };
570
705
  const result = await client.createStep(this.journey._id, payload);
571
706
  this._id = result.id;
@@ -573,26 +708,59 @@ var JourneyStepBuilder = class extends WorkspaceBuilderResource {
573
708
  this._created = true;
574
709
  }
575
710
  };
711
+ var StartStepBuilder = class extends JourneyStepBuilder {
712
+ get isImplicit() {
713
+ return true;
714
+ }
715
+ planLine() {
716
+ return "Step: Start (auto-created)";
717
+ }
718
+ async apply() {
719
+ }
720
+ };
576
721
  var JourneyBuilder = class extends WorkspaceBuilderResource {
577
722
  constructor(name, options = {}) {
578
723
  super();
579
724
  this.name = name;
580
725
  this.description = options.description ?? "";
726
+ this.start = new StartStepBuilder("Start", this, 0);
727
+ this.steps.push(this.start);
581
728
  }
582
729
  name;
583
730
  description;
584
731
  steps = [];
585
732
  goals = [];
733
+ /** The auto-created `Start` step — connect the first real step to it. */
734
+ start;
735
+ /** `parentId:childId` for every edge the server already has (see `adopt`). */
736
+ existingEdges = /* @__PURE__ */ new Set();
586
737
  step(name, options = {}) {
587
738
  const step = new JourneyStepBuilder(
588
739
  name,
589
740
  this,
590
- options.order ?? 0,
591
- options.description ?? ""
741
+ options.order ?? this.steps.length,
742
+ options.description ?? "",
743
+ options
592
744
  );
593
745
  this.steps.push(step);
594
746
  return step;
595
747
  }
748
+ /**
749
+ * Declare a linear cadence: `Start → steps[0] → steps[1] → …`, each edge
750
+ * carrying the matching entry from `spacing`.
751
+ *
752
+ * Chaining is not optional polish. A step created with no incoming edge is
753
+ * a ROOT, and every root fires the moment a member enrols — a "three
754
+ * follow-ups over four days" journey built without edges sends all three
755
+ * at once.
756
+ */
757
+ sequence(steps, spacing = []) {
758
+ let parent = this.start;
759
+ steps.forEach((step, index) => {
760
+ parent = parent.connectsTo(step, spacing[index] ?? {});
761
+ });
762
+ return this;
763
+ }
596
764
  goal(name, options = {}) {
597
765
  this.goals.push({
598
766
  name,
@@ -600,17 +768,85 @@ var JourneyBuilder = class extends WorkspaceBuilderResource {
600
768
  step: options.step,
601
769
  completionCel: options.completionCel ?? options.completion_cel,
602
770
  value: options.value,
603
- order: options.order ?? 0
771
+ order: options.order ?? this.goals.length
604
772
  });
605
773
  return this;
606
774
  }
775
+ /**
776
+ * Authoring problems that make a journey silently misbehave at runtime.
777
+ * Reported at plan/preview time, before anything is written.
778
+ */
779
+ validate() {
780
+ const problems = [];
781
+ const withParents = /* @__PURE__ */ new Set();
782
+ for (const step of this.steps) {
783
+ for (const connection of step.connections) withParents.add(connection.child);
784
+ }
785
+ for (const step of this.steps) {
786
+ if (step === this.start) continue;
787
+ if (!withParents.has(step)) {
788
+ problems.push(
789
+ `step '${step.name}' has no incoming edge \u2014 it is a root and fires for every member the moment they enrol. Connect it from journey.start or a prior step.`
790
+ );
791
+ }
792
+ if (!step.hasOutreach) {
793
+ problems.push(
794
+ `step '${step.name}' has no JOURNEY_STEP_EXECUTE action \u2014 a member reaching it gets nothing. Add one with .sendsSms / .callsMember / .sendsEmail / .onExecute.`
795
+ );
796
+ }
797
+ }
798
+ if (!this.goals.length) {
799
+ problems.push(`journey '${this.name}' has no goals \u2014 nothing measures whether it works.`);
800
+ }
801
+ return problems;
802
+ }
803
+ /**
804
+ * Adopt an existing journey, its steps, and its EDGES, matched by name.
805
+ *
806
+ * Re-running a builder script is normal — a program is usually built in
807
+ * stages as provisioning lands — and without adoption the second run would
808
+ * create a duplicate journey and a duplicate set of steps beside the live
809
+ * one, with members enrolled in neither.
810
+ *
811
+ * The edge set is recorded from the server's own `childConnections` rather
812
+ * than inferred from "both steps exist". A first run that created the steps
813
+ * and then failed before writing an edge leaves exactly that state, and
814
+ * inferring would skip the missing edge on every subsequent run — leaving a
815
+ * permanent root step that fires for every member at enrolment, silently.
816
+ * The journey detail read eager-loads connections, so the real answer is
817
+ * already in hand; there is no reason to guess.
818
+ */
819
+ adopt(existing) {
820
+ super.adopt(existing);
821
+ const existingSteps = existing.steps ?? [];
822
+ const byName = new Map(existingSteps.map((step) => [step.name, step]));
823
+ const startStep = existingSteps.find((step) => step.id === existing.startStepId) ?? byName.get("Start");
824
+ if (startStep) this.start.adopt(startStep);
825
+ for (const step of this.steps) {
826
+ if (step === this.start) continue;
827
+ const match = byName.get(step.name);
828
+ if (match) step.adopt(match);
829
+ }
830
+ for (const step of existingSteps) {
831
+ for (const edge of step.childConnections ?? []) {
832
+ this.existingEdges.add(edgeKey(edge.parentStepId, edge.childStepId));
833
+ }
834
+ }
835
+ }
836
+ /** True when the server already has this exact edge. */
837
+ hasEdge(parent, child) {
838
+ if (parent._id === void 0 || child._id === void 0) return false;
839
+ return this.existingEdges.has(edgeKey(parent._id, child._id));
840
+ }
607
841
  planLine() {
608
842
  const parts = [];
609
- if (this.steps.length) parts.push(`${this.steps.length} steps`);
843
+ const real = this.steps.filter((step) => step !== this.start);
844
+ if (real.length) parts.push(`${real.length} steps`);
610
845
  if (this.goals.length) parts.push(`${this.goals.length} goals`);
611
846
  return `Journey: ${this.name}${parts.length ? ` (${parts.join(", ")})` : ""}`;
612
847
  }
613
848
  async apply(client) {
849
+ if (this._existed) return;
614
850
  const result = await client.createJourney({
615
851
  name: this.name,
616
852
  description: this.description
@@ -618,6 +854,15 @@ var JourneyBuilder = class extends WorkspaceBuilderResource {
618
854
  this._id = result.id;
619
855
  this._uuid = result.uuid;
620
856
  this._created = true;
857
+ const startStep = (result.steps ?? []).find(
858
+ (step) => step.id === result.startStepId || step.name === "Start"
859
+ );
860
+ if (!startStep) {
861
+ throw new Error(
862
+ `Journey '${this.name}' was created without a Start step; edges from journey.start cannot be wired.`
863
+ );
864
+ }
865
+ this.start.adopt(startStep);
621
866
  }
622
867
  };
623
868
  function buildJourneyConnectionPayload(connection) {
@@ -627,7 +872,9 @@ function buildJourneyConnectionPayload(connection) {
627
872
  childStepUuid: connection.child._uuid,
628
873
  ...connection.condition ? { celCondition: connection.condition } : {},
629
874
  ...connection.required ? { required: true } : {},
630
- ...connection.timeoutSeconds ? { timeoutSeconds: connection.timeoutSeconds } : {}
875
+ ...connection.timeoutSeconds ? { timeoutSeconds: connection.timeoutSeconds } : {},
876
+ ...connection.order !== void 0 ? { order: connection.order } : {},
877
+ ...connection.filterId !== void 0 ? { filterId: connection.filterId } : {}
631
878
  };
632
879
  }
633
880
  function buildJourneyGoalPayload(goal) {
@@ -731,6 +978,16 @@ var TaskBuilder = class extends WorkspaceBuilderResource {
731
978
  description;
732
979
  transitions = [];
733
980
  eventRules = [];
981
+ /** Child task ids the server already has, recorded when this task is adopted. */
982
+ existingChildIds = /* @__PURE__ */ new Set();
983
+ /** Record the edges an adopted task already carries, so a re-run adds only what is missing. */
984
+ adopt(existing) {
985
+ super.adopt(existing);
986
+ for (const child of existing.childTasks ?? []) {
987
+ const id = child.taskId ?? child.id;
988
+ if (typeof id === "number") this.existingChildIds.add(id);
989
+ }
990
+ }
734
991
  transitionsTo(child, when, autoNext = false, required = false, celCondition) {
735
992
  if (child === this) {
736
993
  throw new Error(`Task '${this.name}' cannot transition to itself`);
@@ -913,6 +1170,15 @@ var PHASES = /* @__PURE__ */ new Map([
913
1170
  [JourneyBuilder, 7],
914
1171
  [JourneyStepBuilder, 8]
915
1172
  ]);
1173
+ function phaseFor(resource) {
1174
+ let ctor = resource.constructor;
1175
+ while (ctor) {
1176
+ const phase = PHASES.get(ctor);
1177
+ if (phase !== void 0) return phase;
1178
+ ctor = Object.getPrototypeOf(ctor);
1179
+ }
1180
+ return 99;
1181
+ }
916
1182
  var WorkspaceBuilder = class {
917
1183
  constructor(client) {
918
1184
  this.client = client;
@@ -975,7 +1241,8 @@ var WorkspaceBuilder = class {
975
1241
  ${"=".repeat(50)}`);
976
1242
  writeLine("Plan");
977
1243
  writeLine("=".repeat(50));
978
- for (const resource of resources) {
1244
+ const declared = resources.filter((resource) => !resource.isImplicit);
1245
+ for (const resource of declared) {
979
1246
  writeLine(` + ${resource.planLine()}`);
980
1247
  }
981
1248
  const wiring = this.collectWiringSummary(resources);
@@ -983,8 +1250,9 @@ ${"=".repeat(50)}`);
983
1250
  writeLine("\n Wiring:");
984
1251
  for (const line of wiring) writeLine(` ${line}`);
985
1252
  }
1253
+ this.reportJourneyWarnings();
986
1254
  writeLine(`
987
- Total: ${resources.length} resources, ${wiring.length} connections`);
1255
+ Total: ${declared.length} resources, ${wiring.length} connections`);
988
1256
  writeLine(`${"=".repeat(50)}
989
1257
  `);
990
1258
  }
@@ -995,7 +1263,8 @@ ${"=".repeat(50)}`);
995
1263
  ${"=".repeat(50)}`);
996
1264
  writeLine("Preview");
997
1265
  writeLine("=".repeat(50));
998
- for (const resource of resources) {
1266
+ const declared = resources.filter((resource) => !resource.isImplicit);
1267
+ for (const resource of declared) {
999
1268
  if (resource._existed) {
1000
1269
  writeLine(` = ${resource.planLine()} (exists, id=${resource._id})`);
1001
1270
  } else {
@@ -1007,8 +1276,9 @@ ${"=".repeat(50)}`);
1007
1276
  writeLine("\n Wiring:");
1008
1277
  for (const line of wiring) writeLine(` ${line}`);
1009
1278
  }
1010
- const toCreate = resources.filter((resource) => !resource._existed);
1011
- const existing = resources.filter((resource) => resource._existed);
1279
+ this.reportJourneyWarnings();
1280
+ const toCreate = declared.filter((resource) => !resource._existed);
1281
+ const existing = declared.filter((resource) => resource._existed);
1012
1282
  writeLine(
1013
1283
  `
1014
1284
  Total: ${toCreate.length} to create, ${existing.length} existing, ${wiring.length} connections`
@@ -1016,13 +1286,72 @@ ${"=".repeat(50)}`);
1016
1286
  writeLine(`${"=".repeat(50)}
1017
1287
  `);
1018
1288
  }
1289
+ /**
1290
+ * Print journey authoring problems that only bite at runtime — a step with
1291
+ * no incoming edge fires for every member at enrolment, and a step with no
1292
+ * action does nothing at all. Both look fine in the editor, so they are
1293
+ * surfaced before the write rather than discovered from a member's phone.
1294
+ */
1295
+ reportJourneyWarnings() {
1296
+ const problems = this.resources.filter((resource) => resource instanceof JourneyBuilder).flatMap((journey) => journey.validate());
1297
+ if (!problems.length) return;
1298
+ writeLine("\n Journey warnings:");
1299
+ for (const problem of problems) writeLine(` ! ${problem}`);
1300
+ }
1301
+ /**
1302
+ * Problems that make the plan unwritable, found before the first write.
1303
+ *
1304
+ * The API validates each CEL when you save it, and its diagnosis is right —
1305
+ * but it fires per-write, so a plan whose edges reference a non-collection
1306
+ * form gets four labels, two forms, a workflow and six tasks created before
1307
+ * the seventh call rejects it. The operator is left with a half-built
1308
+ * program to hand-repair. Checking here costs nothing and writes nothing.
1309
+ *
1310
+ * Deliberately narrow: only cross-resource facts the builder can see with
1311
+ * certainty. Per-field CEL schema checking stays server-side, where the
1312
+ * real validator lives — duplicating it here would drift.
1313
+ */
1314
+ preflight() {
1315
+ const problems = [];
1316
+ const forms = this.resources.filter(
1317
+ (resource) => resource instanceof FormBuilder
1318
+ );
1319
+ const collectionBySlug = new Map(forms.map((form) => [form.slug, form.collection]));
1320
+ const reference = /assignment\.collections\.([A-Za-z0-9_]+)/g;
1321
+ for (const resource of this.sortedResources()) {
1322
+ if (!(resource instanceof TaskBuilder)) continue;
1323
+ for (const transition of resource.transitions) {
1324
+ const cel = transition.celCondition;
1325
+ if (!cel) continue;
1326
+ for (const [, slug] of cel.matchAll(reference)) {
1327
+ if (!collectionBySlug.has(slug)) continue;
1328
+ if (!collectionBySlug.get(slug)) {
1329
+ problems.push(
1330
+ `task '${transition.parent.name}' \u2192 '${transition.child.name}': CEL reads assignment.collections.${slug}, but this plan creates '${slug}' as a non-collection form. Declare it with { collection: true }.`
1331
+ );
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ return [...new Set(problems)];
1337
+ }
1019
1338
  async apply() {
1339
+ const problems = this.preflight();
1340
+ if (problems.length) {
1341
+ writeLine("\nPlan rejected \u2014 nothing was created:");
1342
+ for (const problem of problems) writeLine(` ! ${problem}`);
1343
+ throw new Error(
1344
+ `Plan has ${problems.length} problem(s) and was not applied:
1345
+ ` + problems.map((problem) => ` - ${problem}`).join("\n")
1346
+ );
1347
+ }
1020
1348
  const resources = this.sortedResources();
1021
1349
  await this.resolveExisting(resources);
1022
1350
  const summary = {};
1023
1351
  writeLine("Applying...");
1024
1352
  for (const resource of resources) {
1025
1353
  try {
1354
+ if (resource.isImplicit) continue;
1026
1355
  if (resource._existed) {
1027
1356
  const key = resourceKey(resource);
1028
1357
  summary[key] ??= [];
@@ -1080,6 +1409,29 @@ Done! Created ${count} resources.`);
1080
1409
  }
1081
1410
  }
1082
1411
  }
1412
+ const filterNames = new Map(
1413
+ resources.filter(
1414
+ (resource) => resource instanceof MemberFilterBuilder
1415
+ ).map((resource) => [resource.name, resource])
1416
+ );
1417
+ if (filterNames.size) {
1418
+ for (const existing of await this.client.getMemberFilters()) {
1419
+ if (existing.name && filterNames.has(existing.name)) {
1420
+ filterNames.get(existing.name).adopt(existing);
1421
+ }
1422
+ }
1423
+ }
1424
+ const journeyNames = new Map(
1425
+ resources.filter((resource) => resource instanceof JourneyBuilder).map((resource) => [resource.name, resource])
1426
+ );
1427
+ if (journeyNames.size) {
1428
+ const existingJourneys = await this.client.getJourneys();
1429
+ for (const existing of existingJourneys.items) {
1430
+ if (!existing.name || !journeyNames.has(existing.name)) continue;
1431
+ const detail = await this.client.getJourney(existing.id);
1432
+ journeyNames.get(existing.name).adopt(detail);
1433
+ }
1434
+ }
1083
1435
  if (!workflowNames.size) return;
1084
1436
  const existingWorkflows = await this.client.getWorkflows();
1085
1437
  for (const { workflow: existing } of existingWorkflows.items) {
@@ -1130,9 +1482,7 @@ Done! Created ${count} resources.`);
1130
1482
  all.push(...resource.steps);
1131
1483
  }
1132
1484
  }
1133
- return all.sort(
1134
- (a, b) => (PHASES.get(a.constructor) ?? 99) - (PHASES.get(b.constructor) ?? 99)
1135
- );
1485
+ return all.sort((a, b) => phaseFor(a) - phaseFor(b));
1136
1486
  }
1137
1487
  collectWiringSummary(resources) {
1138
1488
  const lines = [];
@@ -1153,7 +1503,11 @@ Done! Created ${count} resources.`);
1153
1503
  lines.push(`${rule.actionType} rule on '${resource.name}'`);
1154
1504
  } else if (resource instanceof JourneyStepBuilder) {
1155
1505
  for (const connection of resource.connections) {
1156
- lines.push(`${connection.parent.name} -> ${connection.child.name}`);
1506
+ const guard = connection.condition ? ` [${connection.condition}]` : "";
1507
+ lines.push(`${connection.parent.name} -> ${connection.child.name}${guard}`);
1508
+ }
1509
+ for (const rule of resource.rules) {
1510
+ lines.push(`${rule.actionType} on step '${resource.name}'`);
1157
1511
  }
1158
1512
  } else if (resource instanceof JourneyBuilder) {
1159
1513
  for (const goal of resource.goals) lines.push(`Goal: ${goal.name}`);
@@ -1177,6 +1531,7 @@ Done! Created ${count} resources.`);
1177
1531
  const childTasks = transitions.flatMap((transition) => {
1178
1532
  const taskId = transition.child._id;
1179
1533
  if (taskId === void 0 || taskId === parent._id) return [];
1534
+ if (parent.existingChildIds.has(taskId)) return [];
1180
1535
  return [
1181
1536
  {
1182
1537
  taskId,
@@ -1269,6 +1624,10 @@ Done! Created ${count} resources.`);
1269
1624
  if (!(resource instanceof JourneyStepBuilder)) continue;
1270
1625
  for (const connection of resource.connections) {
1271
1626
  if (resource.journey._id === void 0) continue;
1627
+ if (resource.journey.hasEdge(connection.parent, connection.child)) {
1628
+ writeLine(` Exists: ${connection.parent.name} -> ${connection.child.name}`);
1629
+ continue;
1630
+ }
1272
1631
  const payload = buildJourneyConnectionPayload(connection);
1273
1632
  if (!payload) continue;
1274
1633
  await this.client.createConnection(resource.journey._id, payload);
@@ -1279,6 +1638,12 @@ Done! Created ${count} resources.`);
1279
1638
  async applyJourneyGoals(resources) {
1280
1639
  for (const resource of resources) {
1281
1640
  if (!(resource instanceof JourneyBuilder) || resource._id === void 0) continue;
1641
+ if (resource._existed) {
1642
+ for (const goal of resource.goals) {
1643
+ writeLine(` Skipped (journey already exists): Goal: ${goal.name}`);
1644
+ }
1645
+ continue;
1646
+ }
1282
1647
  for (const goal of resource.goals) {
1283
1648
  await this.client.createGoal(resource._id, buildJourneyGoalPayload(goal));
1284
1649
  writeLine(` Created goal: ${goal.name}`);
@@ -1557,6 +1922,7 @@ export {
1557
1922
  MemberType,
1558
1923
  MenuItemResponseItemType,
1559
1924
  MenuItemResponseItemType as MenuItemType,
1925
+ MergeMemberRolesRequestAccessStrategy,
1560
1926
  ModelProvider,
1561
1927
  ModelType,
1562
1928
  NO_ASSISTANT_PARTICIPANT_CHAT_TYPES,
@@ -1579,6 +1945,7 @@ export {
1579
1945
  PIDS_SOURCES,
1580
1946
  POSITIVE_RATING_THRESHOLD,
1581
1947
  ProductType,
1948
+ PromoCodeStatus,
1582
1949
  QUERY_VALUELESS_OPS,
1583
1950
  QueriesApi,
1584
1951
  REALTIME_MODEL_CUSTOM,
@@ -1619,10 +1986,14 @@ export {
1619
1986
  WorkflowAccessWarningResourceType,
1620
1987
  WorkflowBuilder,
1621
1988
  WorkflowAccessWarningResourceType as WorkflowResourceType,
1989
+ WorkflowRoutingMode,
1622
1990
  WorkspaceBuilder,
1991
+ WorkspaceBuilderResource,
1623
1992
  WorkspaceFeature,
1624
1993
  WorkspaceProvisionStatus,
1994
+ archivedMemberConflictId,
1625
1995
  areDependenciesSatisfied,
1996
+ asIdentifiedMemberContactConflict,
1626
1997
  canPreviewVoice,
1627
1998
  clearRegistry,
1628
1999
  createWorkspaceClient,
@@ -1646,9 +2017,12 @@ export {
1646
2017
  getTTSProvider,
1647
2018
  getVoiceModelId,
1648
2019
  initializeFeatures,
2020
+ isAccountContactConflict,
1649
2021
  isAgentNoteOrigin,
1650
2022
  isChatType,
1651
2023
  isFeatureAvailable,
2024
+ isIdentifiedMemberContactConflict,
2025
+ isMemberContactConflict,
1652
2026
  isPositiveRating,
1653
2027
  isRegistryInitialized,
1654
2028
  isSMSDeliveryFailureStatus,