@elitedcs/ghl-mcp 3.46.0 → 3.47.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 (3) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.js +131 -45
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.47.0 — Blueprint Cap-0: cross-workflow exit chaining + native trigger builders
4
+
5
+ Two foundational executor fixes so `apply_build_plan` builds workflows that actually
6
+ chain together AND actually fire — the prerequisites for a non-hollow account build.
7
+
8
+ - **Fixed: a cross-workflow `remove_from_workflow` / `add_to_workflow` halted the build.**
9
+ When one workflow's exit action targeted another workflow in the SAME plan, execute
10
+ halted, reporting the (already-built) target as unresolved — so the canonical "pull the
11
+ lead out of nurture when they book / reply" pattern (workflows that enrol each other)
12
+ could not fully execute. The executor now resolves EVERY workflow's identity in a first
13
+ pass (never-clobber bind existing, else create an empty DRAFT) before expanding any
14
+ actions, so forward, backward, AND mutual cross-workflow references all resolve. Safety
15
+ rails intact and hardened: never-clobber (existing workflows are never modified),
16
+ empty-orphan detection (now halts rather than blind-binds when an existing workflow's
17
+ action count is unreadable), verify-after, and rollback of every unsaved shell on a halt
18
+ — with any orphan id that could not be deleted SURFACED in the halt message (never
19
+ swallowed) and dropped from the returned id map.
20
+ - **New: native trigger builders for the trigger types real accounts actually use.**
21
+ Previously only `contact_tag` triggers were auto-built; every other workflow shipped
22
+ trigger-less (inactive) — the #1 reason builds were hollow. Blueprint now builds
23
+ `form_submission`, `appointment` (status-conditioned, e.g. confirmed / no-show),
24
+ `customer_reply`, `pipeline_stage_updated`, `inbound_webhook`, and `payment_received`
25
+ triggers natively. Each saved shape was CAPTURED from a real, live, UI-built trigger in a
26
+ reference account (not guessed) so the synthesized trigger reads back identical and fires.
27
+ Refs in trigger conditions (form / pipeline / stage) resolve to real GHL ids at build
28
+ time. A trigger type Blueprint doesn't yet build natively — or one missing the field it
29
+ needs (e.g. an `appointment` trigger with no status) — is still surfaced as a manual step.
30
+ - Plan validation now rejects two workflows that share a name (the executor binds workflows
31
+ by name, so duplicates would collapse onto one shell).
32
+
3
33
  ## 3.46.0 — `verify_funnel`: prove a funnel actually captures leads (+ external-funnel wiring bundle)
4
34
 
5
35
  The live-runtime companion to `audit_workflows`. `audit_workflows` finds dead workflow
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.46.0",
34
+ version: "3.47.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
36
  description: "GoHighLevel MCP Server for Claude. 220 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
@@ -11161,13 +11161,16 @@ var actionSchema = import_zod53.z.discriminatedUnion("type", [
11161
11161
  action: import_zod53.z.enum(["exit", "continue", "wait"]).optional()
11162
11162
  })
11163
11163
  ]);
11164
+ var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
11164
11165
  var triggerSchema = import_zod53.z.object({
11165
11166
  type: import_zod53.z.string(),
11166
11167
  formRef: nsRef("form").optional(),
11167
11168
  tagRef: nsRef("tag").optional(),
11168
11169
  calendarRef: nsRef("calendar").optional(),
11169
11170
  pipelineRef: nsRef("pipeline").optional(),
11170
- stageRef: nsRef("stage").optional()
11171
+ stageRef: nsRef("stage").optional(),
11172
+ // Required for a native `appointment` trigger (the status it fires on).
11173
+ appointmentStatus: import_zod53.z.enum(APPOINTMENT_STATUSES).optional()
11171
11174
  });
11172
11175
  var workflowSchema = import_zod53.z.object({
11173
11176
  ref: nsRef("workflow"),
@@ -11376,7 +11379,12 @@ function validateBuildPlan(input) {
11376
11379
  ["customValues", (plan.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
11377
11380
  ["calendars", (plan.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
11378
11381
  ["forms", (plan.forms ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
11379
- ["funnels", (plan.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name }))]
11382
+ ["funnels", (plan.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
11383
+ // Workflows bind by name too: the executor's two-pass build resolves each
11384
+ // workflow's identity by name (bind same-named existing, else create a shell).
11385
+ // Two plan workflows sharing a name would make the second bind to the FIRST's
11386
+ // freshly-created shell — only one would ever be saved. Reject before any write.
11387
+ ["workflows", (plan.workflows ?? []).map((x) => ({ ref: x.ref, name: x.name }))]
11380
11388
  ];
11381
11389
  for (const [type, objs] of nameGroups) {
11382
11390
  const seen = /* @__PURE__ */ new Map();
@@ -11760,23 +11768,70 @@ function triggerPlainEnglish(trigger, idx) {
11760
11768
  return `trigger type "${trigger.type}"`;
11761
11769
  }
11762
11770
  }
11763
- function expandTrigger(trigger, idx) {
11764
- if (trigger.type === "contact_tag") {
11765
- if (!trigger.tagRef) return { kind: "manual", reason: "Set this workflow's contact-tag trigger in the GHL UI (the plan trigger has no tag)." };
11766
- const tagName = idx.tagName.get(trigger.tagRef) ?? trigger.tagRef;
11767
- return {
11768
- kind: "native",
11769
- trigger: {
11771
+ function expandTrigger(trigger, idx, idMap) {
11772
+ const manual = (reason) => ({ kind: "manual", reason });
11773
+ const native = (t, pendingRefs = []) => ({ kind: "native", trigger: t, pendingRefs });
11774
+ switch (trigger.type) {
11775
+ case "contact_tag": {
11776
+ if (!trigger.tagRef) return manual("Set this workflow's contact-tag trigger in the GHL UI (the plan trigger has no tag).");
11777
+ const tagName = idx.tagName.get(trigger.tagRef) ?? trigger.tagRef;
11778
+ return native({
11770
11779
  name: "Contact Tag",
11771
11780
  type: "contact_tag",
11772
11781
  conditions: [{ operator: "index-of-true", field: "tagsAdded", value: tagName, title: "Tag Added", type: "select", id: "tag-added" }]
11773
- }
11774
- };
11782
+ });
11783
+ }
11784
+ case "form_submission":
11785
+ case "form_submitted": {
11786
+ if (!trigger.formRef) return manual("Set this workflow's form-submission trigger in the GHL UI (the plan trigger has no form).");
11787
+ const formId = resolveId(trigger.formRef, idMap);
11788
+ return native({
11789
+ name: "Form Submitted",
11790
+ type: "form_submission",
11791
+ conditions: [{ operator: "is-any-of", field: "form.id", value: [formId], title: "Form is", type: "string" }]
11792
+ }, isPending(formId) ? [trigger.formRef] : []);
11793
+ }
11794
+ case "appointment": {
11795
+ const status = trigger.appointmentStatus;
11796
+ if (!status) return manual(`Set this workflow's appointment trigger in the GHL UI: ${triggerPlainEnglish(trigger, idx)} (the plan trigger has no appointmentStatus to fire on, e.g. confirmed / noshow).`);
11797
+ return native({
11798
+ name: `Appointment Status \u2014 ${status}`,
11799
+ type: "appointment",
11800
+ conditions: [
11801
+ { operator: "==", field: "appointment.eventType", value: "normal", title: "Event Type", type: "select" },
11802
+ { operator: "==", field: "appointment.status", value: status, title: "Appointment status is", type: "select" }
11803
+ ]
11804
+ });
11805
+ }
11806
+ case "customer_reply":
11807
+ case "contact_replied": {
11808
+ return native({ name: "Customer Replied", type: "customer_reply", conditions: [] });
11809
+ }
11810
+ case "pipeline_stage_updated": {
11811
+ if (!trigger.pipelineRef || !trigger.stageRef) return manual("Set this workflow's pipeline-stage trigger in the GHL UI (the plan trigger needs both a pipeline and a stage).");
11812
+ const pipelineId = resolveId(trigger.pipelineRef, idMap);
11813
+ const stageId = resolveId(trigger.stageRef, idMap);
11814
+ const pending = [];
11815
+ if (isPending(pipelineId)) pending.push(trigger.pipelineRef);
11816
+ if (isPending(stageId)) pending.push(trigger.stageRef);
11817
+ return native({
11818
+ name: "Pipeline Stage Changed",
11819
+ type: "pipeline_stage_updated",
11820
+ conditions: [
11821
+ { operator: "==", field: "opportunity.pipelineId", value: pipelineId, title: "In pipeline", type: "select" },
11822
+ { operator: "==", field: "opportunity.pipelineStageId", value: stageId, title: "Pipeline stage", type: "select" }
11823
+ ]
11824
+ }, pending);
11825
+ }
11826
+ case "inbound_webhook": {
11827
+ return native({ name: "Inbound Webhook", type: "inbound_webhook", conditions: [] });
11828
+ }
11829
+ case "payment_received": {
11830
+ return native({ name: "Payment Received", type: "payment_received", conditions: [] });
11831
+ }
11832
+ default:
11833
+ return manual(`Set this workflow's trigger in the GHL UI: ${triggerPlainEnglish(trigger, idx)}, then publish. (Blueprint builds the workflow + all its steps as DRAFT; this trigger type isn't auto-built yet.)`);
11775
11834
  }
11776
- return {
11777
- kind: "manual",
11778
- reason: `Set this workflow's trigger in the GHL UI: ${triggerPlainEnglish(trigger, idx)}, then publish. (Blueprint builds the workflow + all its steps as DRAFT; only this trigger type isn't auto-built yet.)`
11779
- };
11780
11835
  }
11781
11836
  function expandWorkflow(workflow, idx, idMap, gatedBy) {
11782
11837
  const nativeActions = [];
@@ -11798,9 +11853,13 @@ function expandWorkflow(workflow, idx, idMap, gatedBy) {
11798
11853
  let nativeTrigger;
11799
11854
  let triggerManual;
11800
11855
  if (workflow.trigger) {
11801
- const t = expandTrigger(workflow.trigger, idx);
11802
- if (t.kind === "native") nativeTrigger = t.trigger;
11803
- else triggerManual = t.reason;
11856
+ const t = expandTrigger(workflow.trigger, idx, idMap);
11857
+ if (t.kind === "native") {
11858
+ nativeTrigger = t.trigger;
11859
+ t.pendingRefs.forEach((r) => pendingRefs.add(r));
11860
+ } else {
11861
+ triggerManual = t.reason;
11862
+ }
11804
11863
  }
11805
11864
  return {
11806
11865
  ref: workflow.ref,
@@ -12375,35 +12434,56 @@ async function executeBackbone(plan, deps, opts = {}) {
12375
12434
  }
12376
12435
  const wfIndex = buildRefIndex(plan);
12377
12436
  const wfIdMap = new Map(Object.entries(idMap));
12378
- for (const wf of plan.workflows ?? []) {
12437
+ const planWorkflows = plan.workflows ?? [];
12438
+ const createdSlots = [];
12439
+ const savedWfIds = /* @__PURE__ */ new Set();
12440
+ const rollbackUnsavedShells = async () => {
12441
+ const failedDeletes = [];
12442
+ for (const slot of createdSlots) {
12443
+ if (savedWfIds.has(slot.id)) continue;
12444
+ try {
12445
+ await deps.deleteWorkflow(slot.id);
12446
+ } catch {
12447
+ failedDeletes.push(slot.id);
12448
+ }
12449
+ delete idMap[slot.ref];
12450
+ wfIdMap.delete(slot.ref);
12451
+ }
12452
+ return failedDeletes;
12453
+ };
12454
+ const orphanSuffix = (failed) => failed.length ? ` WARNING: could not delete orphan shell workflow(s) ${failed.join(", ")} \u2014 delete them in GHL before re-running.` : "";
12455
+ const toSave = [];
12456
+ for (const wf of planWorkflows) {
12379
12457
  let workflows;
12380
12458
  try {
12381
12459
  workflows = await deps.listWorkflows();
12382
12460
  } catch (e) {
12383
- return halt(wf.ref, "workflow", `could not read existing workflows: ${msg(e)}`);
12461
+ const f = await rollbackUnsavedShells();
12462
+ return halt(wf.ref, "workflow", `could not read existing workflows: ${msg(e)}.${orphanSuffix(f)}`);
12384
12463
  }
12385
12464
  const matches = workflows.filter((w) => norm2(w.name) === norm2(wf.name));
12386
12465
  if (matches.length > 1) {
12387
- return halt(wf.ref, "workflow", `${matches.length} existing workflows are named "${wf.name}" \u2014 ambiguous, cannot safely bind ${wf.ref}. Resolve the duplicate in GHL, then re-run.`);
12388
- }
12389
- const expansion = expandWorkflow(wf, wfIndex, wfIdMap, []);
12390
- if (expansion.pendingRefs.length) {
12391
- return halt(wf.ref, "workflow", `workflow "${wf.name}" references objects that weren't built/resolved: ${expansion.pendingRefs.join(", ")}. Cannot build it.`);
12466
+ const f = await rollbackUnsavedShells();
12467
+ return halt(wf.ref, "workflow", `${matches.length} existing workflows are named "${wf.name}" \u2014 ambiguous, cannot safely bind ${wf.ref}. Resolve the duplicate in GHL, then re-run.${orphanSuffix(f)}`);
12392
12468
  }
12393
12469
  if (matches.length === 1) {
12394
12470
  const exId = matches[0].id;
12395
- if (expansion.nativeActions.length > 0) {
12396
- let existingCount = -1;
12471
+ const planHasActions = expandWorkflow(wf, wfIndex, wfIdMap, []).nativeActions.length > 0;
12472
+ if (planHasActions) {
12473
+ let existingCount;
12397
12474
  try {
12398
12475
  existingCount = await deps.getWorkflowActionCount(exId);
12399
- } catch {
12400
- existingCount = -1;
12476
+ } catch (e) {
12477
+ const f = await rollbackUnsavedShells();
12478
+ return halt(wf.ref, "workflow", `could not read the existing workflow named "${wf.name}" to tell whether it's an empty orphan (${msg(e)}) \u2014 refusing to bind it blindly (Blueprint never modifies an existing workflow, and binding an orphan would ship a stepless workflow). Re-run.${orphanSuffix(f)}`);
12401
12479
  }
12402
12480
  if (existingCount === 0) {
12403
- return halt(wf.ref, "workflow", `a workflow named "${wf.name}" already exists but has NO actions \u2014 likely an empty orphan from a failed prior run (or a name collision). Delete it in GHL, then re-run. (Blueprint never modifies an existing workflow.)`);
12481
+ const f = await rollbackUnsavedShells();
12482
+ return halt(wf.ref, "workflow", `a workflow named "${wf.name}" already exists but has NO actions \u2014 likely an empty orphan from a failed prior run (or a name collision). Delete it in GHL, then re-run. (Blueprint never modifies an existing workflow.)${orphanSuffix(f)}`);
12404
12483
  }
12405
12484
  }
12406
12485
  idMap[wf.ref] = exId;
12486
+ wfIdMap.set(wf.ref, exId);
12407
12487
  built.push({ ref: wf.ref, type: "workflow", name: wf.name, status: "existing", realId: exId });
12408
12488
  continue;
12409
12489
  }
@@ -12411,9 +12491,24 @@ async function executeBackbone(plan, deps, opts = {}) {
12411
12491
  try {
12412
12492
  wfId = await deps.createWorkflow(wf.name);
12413
12493
  } catch (e) {
12414
- return halt(wf.ref, "workflow", `create failed: ${msg(e)}`);
12494
+ const f = await rollbackUnsavedShells();
12495
+ return halt(wf.ref, "workflow", `create failed: ${msg(e)}.${orphanSuffix(f)}`);
12496
+ }
12497
+ if (!wfId) {
12498
+ const f = await rollbackUnsavedShells();
12499
+ return halt(wf.ref, "workflow", `workflow create returned no id.${orphanSuffix(f)}`);
12500
+ }
12501
+ createdSlots.push({ ref: wf.ref, id: wfId });
12502
+ idMap[wf.ref] = wfId;
12503
+ wfIdMap.set(wf.ref, wfId);
12504
+ toSave.push({ wf, id: wfId });
12505
+ }
12506
+ for (const { wf, id: wfId } of toSave) {
12507
+ const expansion = expandWorkflow(wf, wfIndex, wfIdMap, []);
12508
+ if (expansion.pendingRefs.length) {
12509
+ const f = await rollbackUnsavedShells();
12510
+ return halt(wf.ref, "workflow", `workflow "${wf.name}" references objects that weren't built/resolved: ${expansion.pendingRefs.join(", ")}. Cannot build it.${orphanSuffix(f)}`);
12415
12511
  }
12416
- if (!wfId) return halt(wf.ref, "workflow", "workflow create returned no id");
12417
12512
  const triggers = expansion.nativeTrigger ? [expansion.nativeTrigger] : [];
12418
12513
  try {
12419
12514
  await deps.saveWorkflow(wfId, { actions: expansion.nativeActions, triggers, stopOnResponse: wf.stopOnResponse });
@@ -12422,19 +12517,10 @@ async function executeBackbone(plan, deps, opts = {}) {
12422
12517
  if (count === 0) throw new Error("saved but no actions persisted (read-after-write)");
12423
12518
  }
12424
12519
  } catch (e) {
12425
- let rolledBack = false;
12426
- try {
12427
- await deps.deleteWorkflow(wfId);
12428
- rolledBack = true;
12429
- } catch {
12430
- }
12431
- return halt(
12432
- wf.ref,
12433
- "workflow",
12434
- rolledBack ? `actions/trigger save failed; the partial workflow was rolled back (deleted): ${msg(e)}` : `actions/trigger save failed AND the rollback delete also failed \u2014 manually delete the orphan workflow (id ${wfId}) in GHL before re-running: ${msg(e)}`
12435
- );
12520
+ const f = await rollbackUnsavedShells();
12521
+ return halt(wf.ref, "workflow", `actions/trigger save failed; the partial workflow${createdSlots.length > 1 ? " (and any other unsaved shells)" : ""} was rolled back: ${msg(e)}.${orphanSuffix(f)}`);
12436
12522
  }
12437
- idMap[wf.ref] = wfId;
12523
+ savedWfIds.add(wfId);
12438
12524
  built.push({ ref: wf.ref, type: "workflow", name: wf.name, status: "created", realId: wfId });
12439
12525
  if (expansion.triggerManual) manual.push({ ref: wf.ref, type: "workflow-trigger", name: wf.name, reason: `[${wf.name}] ${expansion.triggerManual}` });
12440
12526
  for (const nc of expansion.needsContent) manual.push({ ref: wf.ref, type: "workflow-action", name: wf.name, reason: `[${wf.name}] ${nc.reason}` });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.46.0",
3
+ "version": "3.47.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 220 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",