@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.
- package/CHANGELOG.md +30 -0
- package/dist/index.js +131 -45
- 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.
|
|
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
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
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")
|
|
11803
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12426
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|