@elitedcs/ghl-mcp 3.36.0 → 3.37.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/dist/index.js +873 -18
- package/package.json +1 -1
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.37.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
36
|
description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
37
37
|
main: "dist/index.js",
|
|
@@ -6537,8 +6537,8 @@ async function validateLicense(email, licenseKey) {
|
|
|
6537
6537
|
signal: AbortSignal.timeout(1e4)
|
|
6538
6538
|
});
|
|
6539
6539
|
} catch (err) {
|
|
6540
|
-
const
|
|
6541
|
-
return { ok: false, error: `Could not reach license server: ${
|
|
6540
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
6541
|
+
return { ok: false, error: `Could not reach license server: ${msg2}`, reason: "unreachable" };
|
|
6542
6542
|
}
|
|
6543
6543
|
if (res.status >= 500) {
|
|
6544
6544
|
return { ok: false, error: `License server returned HTTP ${res.status}`, reason: "unreachable" };
|
|
@@ -6576,8 +6576,8 @@ async function validateGhl(apiKey2, locationId2) {
|
|
|
6576
6576
|
const name = data?.location?.name || data?.name || "Unknown";
|
|
6577
6577
|
return { ok: true, locationName: name };
|
|
6578
6578
|
} catch (err) {
|
|
6579
|
-
const
|
|
6580
|
-
return { ok: false, error: `Could not reach GHL: ${
|
|
6579
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
6580
|
+
return { ok: false, error: `Could not reach GHL: ${msg2}` };
|
|
6581
6581
|
}
|
|
6582
6582
|
}
|
|
6583
6583
|
async function validateFirebase(firebaseKey, refreshToken) {
|
|
@@ -6593,8 +6593,8 @@ async function validateFirebase(firebaseKey, refreshToken) {
|
|
|
6593
6593
|
const claims = data.id_token ? decodeFirebaseClaims(data.id_token) : {};
|
|
6594
6594
|
return { ok: true, companyId: claims.companyId, userId: claims.userId };
|
|
6595
6595
|
} catch (err) {
|
|
6596
|
-
const
|
|
6597
|
-
return { ok: false, error: `Could not reach Firebase: ${
|
|
6596
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
6597
|
+
return { ok: false, error: `Could not reach Firebase: ${msg2}` };
|
|
6598
6598
|
}
|
|
6599
6599
|
}
|
|
6600
6600
|
function registerSetupTool(server2) {
|
|
@@ -7417,8 +7417,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7417
7417
|
results.success++;
|
|
7418
7418
|
} catch (error) {
|
|
7419
7419
|
results.failed++;
|
|
7420
|
-
const
|
|
7421
|
-
results.errors.push(`${contactId}: ${
|
|
7420
|
+
const msg2 = error instanceof Error ? error.message : String(error);
|
|
7421
|
+
results.errors.push(`${contactId}: ${msg2}`);
|
|
7422
7422
|
}
|
|
7423
7423
|
await delay(200);
|
|
7424
7424
|
}
|
|
@@ -10631,7 +10631,8 @@ var actionSchema = import_zod52.z.discriminatedUnion("type", [
|
|
|
10631
10631
|
import_zod52.z.object({
|
|
10632
10632
|
type: import_zod52.z.literal("goal_event"),
|
|
10633
10633
|
goalCondition: import_zod52.z.string(),
|
|
10634
|
-
|
|
10634
|
+
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
10635
|
+
action: import_zod52.z.enum(["exit", "continue", "wait"]).optional()
|
|
10635
10636
|
})
|
|
10636
10637
|
]);
|
|
10637
10638
|
var triggerSchema = import_zod52.z.object({
|
|
@@ -10793,6 +10794,18 @@ function validateBuildPlan(input) {
|
|
|
10793
10794
|
const { errors, scanned } = checkRefIntegrity(plan, refs);
|
|
10794
10795
|
const dupErrors = duplicates.map((d) => `duplicate ref "${d}" \u2014 refs must be unique`);
|
|
10795
10796
|
const allErrors = [...dupErrors, ...errors];
|
|
10797
|
+
for (const p of plan.pipelines ?? []) {
|
|
10798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10799
|
+
for (const s of p.stages) {
|
|
10800
|
+
const key = s.name.trim().toLowerCase();
|
|
10801
|
+
if (seen.has(key)) {
|
|
10802
|
+
allErrors.push(
|
|
10803
|
+
`pipeline "${p.ref}" has duplicate stage name "${s.name}" \u2014 stage names must be unique within a pipeline so each stage ref resolves to a single real id`
|
|
10804
|
+
);
|
|
10805
|
+
}
|
|
10806
|
+
seen.add(key);
|
|
10807
|
+
}
|
|
10808
|
+
}
|
|
10796
10809
|
const warnings = [];
|
|
10797
10810
|
for (const p of plan.pipelines ?? []) {
|
|
10798
10811
|
const positions = p.stages.map((s) => s.position).sort((a, b) => a - b);
|
|
@@ -10818,6 +10831,584 @@ function validateBuildPlan(input) {
|
|
|
10818
10831
|
};
|
|
10819
10832
|
}
|
|
10820
10833
|
|
|
10834
|
+
// src/intake-to-build/executor.ts
|
|
10835
|
+
var EXECUTION_ORDER = [
|
|
10836
|
+
"pipelines",
|
|
10837
|
+
"customFields",
|
|
10838
|
+
"tags",
|
|
10839
|
+
"customValues",
|
|
10840
|
+
"calendars",
|
|
10841
|
+
"forms",
|
|
10842
|
+
"funnels",
|
|
10843
|
+
"emails",
|
|
10844
|
+
"sms",
|
|
10845
|
+
"workflows"
|
|
10846
|
+
];
|
|
10847
|
+
var WAIT_UNIT_MAP = {
|
|
10848
|
+
minutes: "minutes",
|
|
10849
|
+
hours: "hour",
|
|
10850
|
+
days: "day"
|
|
10851
|
+
};
|
|
10852
|
+
var PENDING = (ref) => `__PENDING__:${ref}`;
|
|
10853
|
+
var isPending = (v) => v.startsWith("__PENDING__:");
|
|
10854
|
+
var norm = (s) => s.trim().toLowerCase();
|
|
10855
|
+
function buildRefIndex(plan) {
|
|
10856
|
+
const idx = {
|
|
10857
|
+
tagName: /* @__PURE__ */ new Map(),
|
|
10858
|
+
fieldName: /* @__PURE__ */ new Map(),
|
|
10859
|
+
fieldType: /* @__PURE__ */ new Map(),
|
|
10860
|
+
email: /* @__PURE__ */ new Map(),
|
|
10861
|
+
sms: /* @__PURE__ */ new Map(),
|
|
10862
|
+
workflowName: /* @__PURE__ */ new Map(),
|
|
10863
|
+
pipelineName: /* @__PURE__ */ new Map(),
|
|
10864
|
+
stageName: /* @__PURE__ */ new Map()
|
|
10865
|
+
};
|
|
10866
|
+
for (const t of plan.tags ?? []) idx.tagName.set(t.ref, t.name);
|
|
10867
|
+
for (const f of plan.customFields ?? []) {
|
|
10868
|
+
idx.fieldName.set(f.ref, f.name);
|
|
10869
|
+
idx.fieldType.set(f.ref, f.dataType);
|
|
10870
|
+
}
|
|
10871
|
+
for (const e of plan.emails ?? []) idx.email.set(e.ref, { subject: e.subject, body: e.body, bodyOutline: e.bodyOutline, name: e.name });
|
|
10872
|
+
for (const s of plan.sms ?? []) idx.sms.set(s.ref, { body: s.body, bodyOutline: s.bodyOutline, name: s.name });
|
|
10873
|
+
for (const w of plan.workflows ?? []) idx.workflowName.set(w.ref, w.name);
|
|
10874
|
+
for (const p of plan.pipelines ?? []) {
|
|
10875
|
+
idx.pipelineName.set(p.ref, p.name);
|
|
10876
|
+
for (const st of p.stages) idx.stageName.set(st.ref, st.name);
|
|
10877
|
+
}
|
|
10878
|
+
return idx;
|
|
10879
|
+
}
|
|
10880
|
+
function htmlWrap(text) {
|
|
10881
|
+
const t = text.trim();
|
|
10882
|
+
if (/^\s*<[a-z]/i.test(t)) return t;
|
|
10883
|
+
return `<p style="margin:0px;">${t}</p>`;
|
|
10884
|
+
}
|
|
10885
|
+
function resolveId(ref, idMap) {
|
|
10886
|
+
const real = idMap.get(ref);
|
|
10887
|
+
return real ?? PENDING(ref);
|
|
10888
|
+
}
|
|
10889
|
+
function expandAction(action, idx, idMap) {
|
|
10890
|
+
switch (action.type) {
|
|
10891
|
+
case "add_contact_tag":
|
|
10892
|
+
case "remove_contact_tag": {
|
|
10893
|
+
const name = idx.tagName.get(action.tagRef) ?? action.tagRef;
|
|
10894
|
+
return {
|
|
10895
|
+
kind: "expanded",
|
|
10896
|
+
pendingRefs: [],
|
|
10897
|
+
native: {
|
|
10898
|
+
type: action.type,
|
|
10899
|
+
name: action.type === "add_contact_tag" ? `Add tag: ${name}` : `Remove tag: ${name}`,
|
|
10900
|
+
attributes: { tags: [name] }
|
|
10901
|
+
}
|
|
10902
|
+
};
|
|
10903
|
+
}
|
|
10904
|
+
case "send_email": {
|
|
10905
|
+
const e = idx.email.get(action.emailRef);
|
|
10906
|
+
if (!e) return { kind: "manual", logicalType: action.type, reason: `email ref ${action.emailRef} not found in plan` };
|
|
10907
|
+
if (!e.body) {
|
|
10908
|
+
return {
|
|
10909
|
+
kind: "needs_content",
|
|
10910
|
+
logicalType: action.type,
|
|
10911
|
+
reason: `email "${action.emailRef}" has only an outline (no send-ready body) \u2014 supply copy before this email step can be built`
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10914
|
+
return {
|
|
10915
|
+
kind: "expanded",
|
|
10916
|
+
pendingRefs: [],
|
|
10917
|
+
native: {
|
|
10918
|
+
type: "email",
|
|
10919
|
+
name: `Email: ${e.name}`,
|
|
10920
|
+
attributes: {
|
|
10921
|
+
subject: e.subject ?? e.name,
|
|
10922
|
+
html: htmlWrap(e.body),
|
|
10923
|
+
trackingOptions: { hasTrackingLinks: false, hasUtmTracking: false, hasTags: false }
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10926
|
+
};
|
|
10927
|
+
}
|
|
10928
|
+
case "send_sms": {
|
|
10929
|
+
const s = idx.sms.get(action.smsRef);
|
|
10930
|
+
if (!s) return { kind: "manual", logicalType: action.type, reason: `sms ref ${action.smsRef} not found in plan` };
|
|
10931
|
+
if (!s.body) {
|
|
10932
|
+
return {
|
|
10933
|
+
kind: "needs_content",
|
|
10934
|
+
logicalType: action.type,
|
|
10935
|
+
reason: `sms "${action.smsRef}" has only an outline (no send-ready body) \u2014 supply copy before this SMS step can be built`
|
|
10936
|
+
};
|
|
10937
|
+
}
|
|
10938
|
+
return {
|
|
10939
|
+
kind: "expanded",
|
|
10940
|
+
pendingRefs: [],
|
|
10941
|
+
native: {
|
|
10942
|
+
type: "sms",
|
|
10943
|
+
name: `SMS: ${s.name}`,
|
|
10944
|
+
attributes: { body: s.body, attachments: [] }
|
|
10945
|
+
}
|
|
10946
|
+
};
|
|
10947
|
+
}
|
|
10948
|
+
case "wait": {
|
|
10949
|
+
return {
|
|
10950
|
+
kind: "expanded",
|
|
10951
|
+
pendingRefs: [],
|
|
10952
|
+
native: {
|
|
10953
|
+
type: "wait",
|
|
10954
|
+
name: "Wait",
|
|
10955
|
+
attributes: {
|
|
10956
|
+
type: "time",
|
|
10957
|
+
startAfter: { type: WAIT_UNIT_MAP[action.unit], value: action.value, when: "after" },
|
|
10958
|
+
name: "Wait",
|
|
10959
|
+
isHybridAction: true,
|
|
10960
|
+
hybridActionType: "wait",
|
|
10961
|
+
convertToMultipath: false,
|
|
10962
|
+
transitions: []
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
};
|
|
10966
|
+
}
|
|
10967
|
+
case "internal_notification": {
|
|
10968
|
+
const looksLikeUserId = /^[A-Za-z0-9]{17,}$/.test(action.to);
|
|
10969
|
+
return {
|
|
10970
|
+
kind: "expanded",
|
|
10971
|
+
pendingRefs: [],
|
|
10972
|
+
native: {
|
|
10973
|
+
type: "internal_notification",
|
|
10974
|
+
name: `Notify: ${action.title}`,
|
|
10975
|
+
attributes: {
|
|
10976
|
+
type: "notification",
|
|
10977
|
+
notification: {
|
|
10978
|
+
body: action.body,
|
|
10979
|
+
title: action.title,
|
|
10980
|
+
userType: "user",
|
|
10981
|
+
redirectPage: "contact",
|
|
10982
|
+
type: "send_notification",
|
|
10983
|
+
selectedUser: looksLikeUserId ? action.to : ""
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
};
|
|
10988
|
+
}
|
|
10989
|
+
case "update_contact_field": {
|
|
10990
|
+
const fieldId = resolveId(action.fieldRef, idMap);
|
|
10991
|
+
const title = idx.fieldName.get(action.fieldRef) ?? action.fieldRef;
|
|
10992
|
+
return {
|
|
10993
|
+
kind: "expanded",
|
|
10994
|
+
pendingRefs: isPending(fieldId) ? [action.fieldRef] : [],
|
|
10995
|
+
native: {
|
|
10996
|
+
type: "update_contact_field",
|
|
10997
|
+
name: `Update field: ${title}`,
|
|
10998
|
+
attributes: {
|
|
10999
|
+
type: "update_contact_field",
|
|
11000
|
+
actionType: "update_field_data",
|
|
11001
|
+
fields: [{ field: fieldId, value: action.value, title, type: "text", date: "" }]
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
};
|
|
11005
|
+
}
|
|
11006
|
+
case "add_notes": {
|
|
11007
|
+
return {
|
|
11008
|
+
kind: "expanded",
|
|
11009
|
+
pendingRefs: [],
|
|
11010
|
+
native: { type: "add_notes", name: "Add note", attributes: { type: "add_notes", html: htmlWrap(action.body) } }
|
|
11011
|
+
};
|
|
11012
|
+
}
|
|
11013
|
+
case "task_notification": {
|
|
11014
|
+
return {
|
|
11015
|
+
kind: "expanded",
|
|
11016
|
+
pendingRefs: [],
|
|
11017
|
+
native: {
|
|
11018
|
+
type: "task-notification",
|
|
11019
|
+
name: `Task: ${action.title}`,
|
|
11020
|
+
attributes: {
|
|
11021
|
+
assignedTo: action.assignedTo ?? "",
|
|
11022
|
+
title: action.title,
|
|
11023
|
+
dueDate: action.dueDate ?? "1",
|
|
11024
|
+
body: action.body ?? "",
|
|
11025
|
+
type: "task-notification",
|
|
11026
|
+
__customInputs__: {}
|
|
11027
|
+
}
|
|
11028
|
+
}
|
|
11029
|
+
};
|
|
11030
|
+
}
|
|
11031
|
+
case "remove_from_workflow":
|
|
11032
|
+
case "add_to_workflow": {
|
|
11033
|
+
const wfId = resolveId(action.workflowRef, idMap);
|
|
11034
|
+
const wfName = idx.workflowName.get(action.workflowRef) ?? action.workflowRef;
|
|
11035
|
+
const pending = isPending(wfId) ? [action.workflowRef] : [];
|
|
11036
|
+
if (action.type === "remove_from_workflow") {
|
|
11037
|
+
return {
|
|
11038
|
+
kind: "expanded",
|
|
11039
|
+
pendingRefs: pending,
|
|
11040
|
+
native: {
|
|
11041
|
+
type: "remove_from_workflow",
|
|
11042
|
+
name: `Remove from: ${wfName}`,
|
|
11043
|
+
attributes: { workflowId: wfId, workflowName: wfName, type: "remove_from_workflow", workflow_id: [wfId] }
|
|
11044
|
+
}
|
|
11045
|
+
};
|
|
11046
|
+
}
|
|
11047
|
+
return {
|
|
11048
|
+
kind: "expanded",
|
|
11049
|
+
pendingRefs: pending,
|
|
11050
|
+
native: {
|
|
11051
|
+
type: "add_to_workflow",
|
|
11052
|
+
name: `Add to: ${wfName}`,
|
|
11053
|
+
attributes: { workflowId: wfId, workflowName: wfName, type: "add_to_workflow", workflow_id: [wfId] }
|
|
11054
|
+
}
|
|
11055
|
+
};
|
|
11056
|
+
}
|
|
11057
|
+
case "create_opportunity":
|
|
11058
|
+
case "update_opportunity": {
|
|
11059
|
+
const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
|
|
11060
|
+
const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
|
|
11061
|
+
const verb = action.type === "create_opportunity" ? "Create" : "Move";
|
|
11062
|
+
return {
|
|
11063
|
+
kind: "manual",
|
|
11064
|
+
logicalType: action.type,
|
|
11065
|
+
reason: `${verb} opportunity \u2192 "${pName}" / stage "${sName}": GHL rejects a synthesized opportunity node ("corrupted type") and silently kills the whole workflow save. Add this step by hand in the GHL workflow builder (Add action \u2192 Create/Update Opportunity), or round-trip an existing node.`
|
|
11066
|
+
};
|
|
11067
|
+
}
|
|
11068
|
+
case "goal_event": {
|
|
11069
|
+
return {
|
|
11070
|
+
kind: "expanded",
|
|
11071
|
+
pendingRefs: [],
|
|
11072
|
+
native: {
|
|
11073
|
+
type: "workflow_goal",
|
|
11074
|
+
name: "Goal",
|
|
11075
|
+
attributes: {
|
|
11076
|
+
op: "or",
|
|
11077
|
+
segments: [{ op: "or", conditions: [{ goal_condition: action.goalCondition, id: "" }] }],
|
|
11078
|
+
type: "workflow_goal",
|
|
11079
|
+
action: action.action ?? "exit"
|
|
11080
|
+
}
|
|
11081
|
+
}
|
|
11082
|
+
};
|
|
11083
|
+
}
|
|
11084
|
+
default: {
|
|
11085
|
+
const _exhaustive = action;
|
|
11086
|
+
return { kind: "manual", logicalType: _exhaustive.type, reason: "unrecognized logical action type" };
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
var MAX_ACTIONS_PER_WORKFLOW = 40;
|
|
11091
|
+
function expandWorkflow(workflow, idx, idMap, gatedBy) {
|
|
11092
|
+
const nativeActions = [];
|
|
11093
|
+
const manual = [];
|
|
11094
|
+
const needsContent = [];
|
|
11095
|
+
const pendingRefs = /* @__PURE__ */ new Set();
|
|
11096
|
+
workflow.actions.forEach((a, i) => {
|
|
11097
|
+
const exp = expandAction(a, idx, idMap);
|
|
11098
|
+
if (exp.kind === "expanded") {
|
|
11099
|
+
nativeActions.push(exp.native);
|
|
11100
|
+
exp.pendingRefs.forEach((r) => pendingRefs.add(r));
|
|
11101
|
+
} else if (exp.kind === "manual") {
|
|
11102
|
+
manual.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
|
|
11103
|
+
} else {
|
|
11104
|
+
needsContent.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
|
|
11105
|
+
}
|
|
11106
|
+
});
|
|
11107
|
+
const splitInto = Math.max(1, Math.ceil(nativeActions.length / MAX_ACTIONS_PER_WORKFLOW));
|
|
11108
|
+
return {
|
|
11109
|
+
ref: workflow.ref,
|
|
11110
|
+
name: workflow.name,
|
|
11111
|
+
nativeActions,
|
|
11112
|
+
manual,
|
|
11113
|
+
needsContent,
|
|
11114
|
+
pendingRefs: [...pendingRefs],
|
|
11115
|
+
splitInto,
|
|
11116
|
+
gatedBy
|
|
11117
|
+
};
|
|
11118
|
+
}
|
|
11119
|
+
function scanSection(section2, planObjects, existing) {
|
|
11120
|
+
const byName = /* @__PURE__ */ new Map();
|
|
11121
|
+
for (const e of existing ?? []) byName.set(norm(e.name), e);
|
|
11122
|
+
return planObjects.map((o) => {
|
|
11123
|
+
const hit = byName.get(norm(o.name));
|
|
11124
|
+
return hit ? { ref: o.ref, type: section2, name: o.name, status: "existing", existingId: hit.id } : { ref: o.ref, type: section2, name: o.name, status: "would_create" };
|
|
11125
|
+
});
|
|
11126
|
+
}
|
|
11127
|
+
function computeWorkflowGates(plan, metHandoffs) {
|
|
11128
|
+
const gates = /* @__PURE__ */ new Map();
|
|
11129
|
+
const addGate = (wfRef, handoffRef) => {
|
|
11130
|
+
const cur = gates.get(wfRef) ?? [];
|
|
11131
|
+
if (!cur.includes(handoffRef)) cur.push(handoffRef);
|
|
11132
|
+
gates.set(wfRef, cur);
|
|
11133
|
+
};
|
|
11134
|
+
const workflowRefs = (plan.workflows ?? []).map((w) => w.ref);
|
|
11135
|
+
for (const h of plan.handoffs ?? []) {
|
|
11136
|
+
if (metHandoffs.has(h.ref)) continue;
|
|
11137
|
+
for (const block of h.blocks ?? []) {
|
|
11138
|
+
if (block.endsWith(".*")) {
|
|
11139
|
+
if (block === "workflow.*") for (const wf of workflowRefs) addGate(wf, h.ref);
|
|
11140
|
+
} else if (refNamespace(block) === "workflow") {
|
|
11141
|
+
addGate(block, h.ref);
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
}
|
|
11145
|
+
return gates;
|
|
11146
|
+
}
|
|
11147
|
+
var SECTION_OBJECTS = {
|
|
11148
|
+
pipelines: (p) => (p.pipelines ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11149
|
+
customFields: (p) => (p.customFields ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11150
|
+
tags: (p) => (p.tags ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11151
|
+
customValues: (p) => (p.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11152
|
+
calendars: (p) => (p.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11153
|
+
forms: (p) => (p.forms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11154
|
+
funnels: (p) => (p.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11155
|
+
emails: (p) => (p.emails ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11156
|
+
sms: (p) => (p.sms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
|
|
11157
|
+
workflows: (p) => (p.workflows ?? []).map((x) => ({ ref: x.ref, name: x.name }))
|
|
11158
|
+
};
|
|
11159
|
+
var SECTION_EXISTING = {
|
|
11160
|
+
pipelines: "pipelines",
|
|
11161
|
+
customFields: "customFields",
|
|
11162
|
+
tags: "tags",
|
|
11163
|
+
customValues: "customValues",
|
|
11164
|
+
calendars: "calendars",
|
|
11165
|
+
forms: "forms",
|
|
11166
|
+
funnels: "funnels",
|
|
11167
|
+
emails: null,
|
|
11168
|
+
// emails/sms live inside workflows; no standalone clobber scan
|
|
11169
|
+
sms: null,
|
|
11170
|
+
workflows: "workflows"
|
|
11171
|
+
};
|
|
11172
|
+
function resolvePlan(plan, existing, opts = {}) {
|
|
11173
|
+
const idx = buildRefIndex(plan);
|
|
11174
|
+
const metHandoffs = new Set(opts.metHandoffs ?? []);
|
|
11175
|
+
const seededIdMap = new Map(Object.entries(opts.idMap ?? {}));
|
|
11176
|
+
const items = [];
|
|
11177
|
+
const idMap = new Map(seededIdMap);
|
|
11178
|
+
for (const section2 of EXECUTION_ORDER) {
|
|
11179
|
+
const objs = SECTION_OBJECTS[section2](plan);
|
|
11180
|
+
if (objs.length === 0) continue;
|
|
11181
|
+
const existingKey = SECTION_EXISTING[section2];
|
|
11182
|
+
const existingList = existingKey ? existing[existingKey] : void 0;
|
|
11183
|
+
const scanned = scanSection(section2, objs, existingList);
|
|
11184
|
+
for (const it of scanned) {
|
|
11185
|
+
items.push(it);
|
|
11186
|
+
if (it.status === "existing" && it.existingId) idMap.set(it.ref, it.existingId);
|
|
11187
|
+
else if (!idMap.has(it.ref)) idMap.set(it.ref, PENDING(it.ref));
|
|
11188
|
+
}
|
|
11189
|
+
if (section2 === "pipelines") {
|
|
11190
|
+
for (const p of plan.pipelines ?? []) {
|
|
11191
|
+
for (const st of p.stages) if (!idMap.has(st.ref)) idMap.set(st.ref, PENDING(st.ref));
|
|
11192
|
+
}
|
|
11193
|
+
}
|
|
11194
|
+
}
|
|
11195
|
+
const gates = computeWorkflowGates(plan, metHandoffs);
|
|
11196
|
+
const workflows = (plan.workflows ?? []).map((w) => expandWorkflow(w, idx, idMap, gates.get(w.ref) ?? []));
|
|
11197
|
+
const handoffs = (plan.handoffs ?? []).map((h) => ({
|
|
11198
|
+
ref: h.ref,
|
|
11199
|
+
owner: h.owner,
|
|
11200
|
+
title: h.title,
|
|
11201
|
+
instruction: h.instruction,
|
|
11202
|
+
successCheck: h.successCheck,
|
|
11203
|
+
met: metHandoffs.has(h.ref),
|
|
11204
|
+
blocks: h.blocks ?? []
|
|
11205
|
+
}));
|
|
11206
|
+
const summary = {
|
|
11207
|
+
wouldCreate: items.filter((i) => i.status === "would_create").length,
|
|
11208
|
+
existing: items.filter((i) => i.status === "existing").length,
|
|
11209
|
+
workflowsTotal: workflows.length,
|
|
11210
|
+
workflowsGated: workflows.filter((w) => w.gatedBy.length > 0).length,
|
|
11211
|
+
actionsExpanded: workflows.reduce((n, w) => n + w.nativeActions.length, 0),
|
|
11212
|
+
actionsManual: workflows.reduce((n, w) => n + w.manual.length, 0),
|
|
11213
|
+
actionsNeedContent: workflows.reduce((n, w) => n + w.needsContent.length, 0)
|
|
11214
|
+
};
|
|
11215
|
+
return { items, idMap: Object.fromEntries(idMap), workflows, handoffs, summary };
|
|
11216
|
+
}
|
|
11217
|
+
function renderReport(plan, result, ctx) {
|
|
11218
|
+
const L = [];
|
|
11219
|
+
const { summary } = result;
|
|
11220
|
+
L.push(`Blueprint build ${ctx.mode === "dry_run" ? "PREVIEW (dry run \u2014 no changes written)" : "REPORT"}`);
|
|
11221
|
+
L.push(`Account: ${ctx.locationName} (${ctx.locationId})`);
|
|
11222
|
+
L.push(`Plan: ${plan.planId} \xB7 preset: ${plan.preset}`);
|
|
11223
|
+
L.push("");
|
|
11224
|
+
L.push(
|
|
11225
|
+
`Objects: ${summary.wouldCreate} to create, ${summary.existing} already exist (skipped, never modified).`
|
|
11226
|
+
);
|
|
11227
|
+
L.push(
|
|
11228
|
+
`Workflows: ${summary.workflowsTotal} (${summary.workflowsGated} gated DRAFT by an unmet handoff). Actions: ${summary.actionsExpanded} auto-built, ${summary.actionsManual} need a manual GHL-UI step, ${summary.actionsNeedContent} need send-ready copy.`
|
|
11229
|
+
);
|
|
11230
|
+
L.push("");
|
|
11231
|
+
L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
|
|
11232
|
+
for (const section2 of EXECUTION_ORDER) {
|
|
11233
|
+
const secItems = result.items.filter((i) => i.type === section2);
|
|
11234
|
+
if (secItems.length === 0) continue;
|
|
11235
|
+
for (const it of secItems) {
|
|
11236
|
+
const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
|
|
11237
|
+
L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
|
|
11238
|
+
}
|
|
11239
|
+
}
|
|
11240
|
+
for (const w of result.workflows) {
|
|
11241
|
+
const gate = w.gatedBy.length ? ` \u2014 DRAFT, gated by ${w.gatedBy.join(", ")}` : ctx.publishWorkflows ? " \u2014 publish" : " \u2014 DRAFT";
|
|
11242
|
+
const split = w.splitInto > 1 ? ` (splits into ${w.splitInto} chained workflows, >${MAX_ACTIONS_PER_WORKFLOW} actions)` : "";
|
|
11243
|
+
L.push(` [workflow] ${w.name}: ${w.nativeActions.length} actions${split}${gate}`);
|
|
11244
|
+
}
|
|
11245
|
+
L.push("");
|
|
11246
|
+
L.push("\u2500\u2500 You must do these by hand (in order) \u2500\u2500");
|
|
11247
|
+
let any = false;
|
|
11248
|
+
for (const w of result.workflows) {
|
|
11249
|
+
for (const m of w.manual) {
|
|
11250
|
+
any = true;
|
|
11251
|
+
L.push(` \u2022 [${w.name}] ${m.reason}`);
|
|
11252
|
+
}
|
|
11253
|
+
for (const nc of w.needsContent) {
|
|
11254
|
+
any = true;
|
|
11255
|
+
L.push(` \u2022 [${w.name}] ${nc.reason}`);
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11258
|
+
for (const h of result.handoffs) {
|
|
11259
|
+
if (h.met) continue;
|
|
11260
|
+
any = true;
|
|
11261
|
+
L.push(` \u2022 [${h.owner}] ${h.title}: ${h.instruction} (done when: ${h.successCheck})`);
|
|
11262
|
+
}
|
|
11263
|
+
if (!any) L.push(" (nothing \u2014 everything in this plan is auto-buildable)");
|
|
11264
|
+
return L.join("\n");
|
|
11265
|
+
}
|
|
11266
|
+
|
|
11267
|
+
// src/intake-to-build/execute.ts
|
|
11268
|
+
var norm2 = (s) => s.trim().toLowerCase();
|
|
11269
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11270
|
+
async function executeBackbone(plan, deps, opts = {}) {
|
|
11271
|
+
const retries = opts.verifyRetries ?? 4;
|
|
11272
|
+
const backoff = opts.verifyBackoffMs ?? 500;
|
|
11273
|
+
const idMap = {};
|
|
11274
|
+
const built = [];
|
|
11275
|
+
const halt = (atRef, type, reason) => ({
|
|
11276
|
+
ok: false,
|
|
11277
|
+
idMap,
|
|
11278
|
+
built,
|
|
11279
|
+
halted: { atRef, type, reason },
|
|
11280
|
+
deferred: deferredSections(plan)
|
|
11281
|
+
});
|
|
11282
|
+
async function pollForNew(read, name, beforeIds) {
|
|
11283
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
11284
|
+
const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
|
|
11285
|
+
if (fresh.length === 1) return fresh[0];
|
|
11286
|
+
if (fresh.length > 1) return void 0;
|
|
11287
|
+
if (attempt < retries) await sleep(backoff * attempt);
|
|
11288
|
+
}
|
|
11289
|
+
return void 0;
|
|
11290
|
+
}
|
|
11291
|
+
for (const p of plan.pipelines ?? []) {
|
|
11292
|
+
let pipelines;
|
|
11293
|
+
try {
|
|
11294
|
+
pipelines = await deps.listPipelines();
|
|
11295
|
+
} catch (e) {
|
|
11296
|
+
return halt(p.ref, "pipeline", `could not read existing pipelines: ${msg(e)}`);
|
|
11297
|
+
}
|
|
11298
|
+
const matches = pipelines.filter((x) => norm2(x.name) === norm2(p.name));
|
|
11299
|
+
if (matches.length > 1) {
|
|
11300
|
+
return halt(p.ref, "pipeline", `${matches.length} existing pipelines are named "${p.name}" \u2014 ambiguous, cannot safely bind ${p.ref}. Resolve the duplicate in GHL or rename, then re-run.`);
|
|
11301
|
+
}
|
|
11302
|
+
let pipeline;
|
|
11303
|
+
let created;
|
|
11304
|
+
if (matches.length === 1) {
|
|
11305
|
+
pipeline = matches[0];
|
|
11306
|
+
created = false;
|
|
11307
|
+
} else {
|
|
11308
|
+
const beforeIds = new Set(pipelines.map((x) => x.id));
|
|
11309
|
+
try {
|
|
11310
|
+
await deps.createPipeline(p.name, p.stages.map((s) => ({ name: s.name, position: s.position })));
|
|
11311
|
+
} catch (e) {
|
|
11312
|
+
return halt(p.ref, "pipeline", `create failed: ${msg(e)}`);
|
|
11313
|
+
}
|
|
11314
|
+
const verified = await pollForNew(() => deps.listPipelines(), p.name, beforeIds);
|
|
11315
|
+
if (!verified) return halt(p.ref, "pipeline", "created but could not verify a single new pipeline by read-back (none, or an ambiguous duplicate, appeared)");
|
|
11316
|
+
pipeline = verified;
|
|
11317
|
+
created = true;
|
|
11318
|
+
}
|
|
11319
|
+
idMap[p.ref] = pipeline.id;
|
|
11320
|
+
built.push({ ref: p.ref, type: "pipeline", name: p.name, status: created ? "created" : "existing", realId: pipeline.id });
|
|
11321
|
+
for (const st of p.stages) {
|
|
11322
|
+
const stageMatches = pipeline.stages.filter((es) => norm2(es.name) === norm2(st.name));
|
|
11323
|
+
if (stageMatches.length === 0) {
|
|
11324
|
+
return halt(st.ref, "stage", `pipeline "${p.name}" has no stage named "${st.name}" after build \u2014 cannot resolve ${st.ref}`);
|
|
11325
|
+
}
|
|
11326
|
+
if (stageMatches.length > 1) {
|
|
11327
|
+
return halt(st.ref, "stage", `pipeline "${p.name}" has ${stageMatches.length} stages named "${st.name}" \u2014 cannot resolve ${st.ref} to a single id`);
|
|
11328
|
+
}
|
|
11329
|
+
idMap[st.ref] = stageMatches[0].id;
|
|
11330
|
+
built.push({ ref: st.ref, type: "stage", name: st.name, status: created ? "created" : "existing", realId: stageMatches[0].id });
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11333
|
+
const fieldHalt = await buildSimple(
|
|
11334
|
+
plan.customFields ?? [],
|
|
11335
|
+
"field",
|
|
11336
|
+
() => deps.listCustomFields(),
|
|
11337
|
+
(f) => deps.createCustomField({ name: f.name, dataType: f.dataType, model: f.model, options: f.options }),
|
|
11338
|
+
(f) => f.name,
|
|
11339
|
+
pollForNew,
|
|
11340
|
+
idMap,
|
|
11341
|
+
built
|
|
11342
|
+
);
|
|
11343
|
+
if (fieldHalt) return halt(fieldHalt.ref, "field", fieldHalt.reason);
|
|
11344
|
+
const tagHalt = await buildSimple(
|
|
11345
|
+
plan.tags ?? [],
|
|
11346
|
+
"tag",
|
|
11347
|
+
() => deps.listTags(),
|
|
11348
|
+
(t) => deps.createTag(t.name),
|
|
11349
|
+
(t) => t.name,
|
|
11350
|
+
pollForNew,
|
|
11351
|
+
idMap,
|
|
11352
|
+
built
|
|
11353
|
+
);
|
|
11354
|
+
if (tagHalt) return halt(tagHalt.ref, "tag", tagHalt.reason);
|
|
11355
|
+
const cvHalt = await buildSimple(
|
|
11356
|
+
plan.customValues ?? [],
|
|
11357
|
+
"cv",
|
|
11358
|
+
() => deps.listCustomValues(),
|
|
11359
|
+
(cv) => deps.createCustomValue(cv.name, cv.value ?? ""),
|
|
11360
|
+
(cv) => cv.name,
|
|
11361
|
+
pollForNew,
|
|
11362
|
+
idMap,
|
|
11363
|
+
built
|
|
11364
|
+
);
|
|
11365
|
+
if (cvHalt) return halt(cvHalt.ref, "cv", cvHalt.reason);
|
|
11366
|
+
return { ok: true, idMap, built, deferred: deferredSections(plan) };
|
|
11367
|
+
}
|
|
11368
|
+
async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
|
|
11369
|
+
if (objects.length === 0) return null;
|
|
11370
|
+
for (const obj of objects) {
|
|
11371
|
+
const name = nameOf(obj);
|
|
11372
|
+
let existing;
|
|
11373
|
+
try {
|
|
11374
|
+
existing = await list();
|
|
11375
|
+
} catch (e) {
|
|
11376
|
+
return { ref: obj.ref, reason: `could not read existing ${type}s: ${msg(e)}` };
|
|
11377
|
+
}
|
|
11378
|
+
const matches = existing.filter((o) => norm2(o.name) === norm2(name));
|
|
11379
|
+
if (matches.length > 1) {
|
|
11380
|
+
return { ref: obj.ref, reason: `${matches.length} existing ${type}s are named "${name}" \u2014 ambiguous, cannot safely bind ${obj.ref}. Resolve the duplicate in GHL, then re-run.` };
|
|
11381
|
+
}
|
|
11382
|
+
if (matches.length === 1) {
|
|
11383
|
+
idMap[obj.ref] = matches[0].id;
|
|
11384
|
+
built.push({ ref: obj.ref, type, name, status: "existing", realId: matches[0].id });
|
|
11385
|
+
continue;
|
|
11386
|
+
}
|
|
11387
|
+
const beforeIds = new Set(existing.map((o) => o.id));
|
|
11388
|
+
try {
|
|
11389
|
+
await create(obj);
|
|
11390
|
+
} catch (e) {
|
|
11391
|
+
return { ref: obj.ref, reason: `create failed: ${msg(e)}` };
|
|
11392
|
+
}
|
|
11393
|
+
const verified = await pollForNew(list, name, beforeIds);
|
|
11394
|
+
if (!verified) return { ref: obj.ref, reason: `created but could not verify a single new ${type} by read-back (none, or an ambiguous duplicate, appeared)` };
|
|
11395
|
+
idMap[obj.ref] = verified.id;
|
|
11396
|
+
built.push({ ref: obj.ref, type, name, status: "created", realId: verified.id });
|
|
11397
|
+
}
|
|
11398
|
+
return null;
|
|
11399
|
+
}
|
|
11400
|
+
function deferredSections(plan) {
|
|
11401
|
+
const out = [];
|
|
11402
|
+
if (plan.calendars?.length) out.push({ section: "calendars", count: plan.calendars.length });
|
|
11403
|
+
if (plan.forms?.length) out.push({ section: "forms", count: plan.forms.length });
|
|
11404
|
+
if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
|
|
11405
|
+
if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
|
|
11406
|
+
return out;
|
|
11407
|
+
}
|
|
11408
|
+
function msg(e) {
|
|
11409
|
+
return e instanceof Error ? e.message : String(e);
|
|
11410
|
+
}
|
|
11411
|
+
|
|
10821
11412
|
// src/tools/intake-to-build.ts
|
|
10822
11413
|
var customFieldItemSchema = import_zod53.z.object({
|
|
10823
11414
|
id: import_zod53.z.string(),
|
|
@@ -10875,10 +11466,135 @@ function findRecordForQuestion(q, records) {
|
|
|
10875
11466
|
const wantName = intakeFieldName(q.label).toLowerCase();
|
|
10876
11467
|
return records.find((r) => r.name.toLowerCase() === wantName);
|
|
10877
11468
|
}
|
|
10878
|
-
var
|
|
11469
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10879
11470
|
function isFormNotYetPropagated(error) {
|
|
10880
|
-
const
|
|
10881
|
-
return /does not exist or is deleted/i.test(
|
|
11471
|
+
const msg2 = error instanceof Error ? error.message : String(error);
|
|
11472
|
+
return /does not exist or is deleted/i.test(msg2);
|
|
11473
|
+
}
|
|
11474
|
+
function pickObjects(raw, keys) {
|
|
11475
|
+
const root = raw && typeof raw === "object" ? raw : {};
|
|
11476
|
+
let list = Array.isArray(raw) ? raw : void 0;
|
|
11477
|
+
if (!list) {
|
|
11478
|
+
for (const k of keys) {
|
|
11479
|
+
if (Array.isArray(root[k])) {
|
|
11480
|
+
list = root[k];
|
|
11481
|
+
break;
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
}
|
|
11485
|
+
if (!Array.isArray(list)) return [];
|
|
11486
|
+
const out = [];
|
|
11487
|
+
for (const item of list) {
|
|
11488
|
+
if (!item || typeof item !== "object") continue;
|
|
11489
|
+
const o = item;
|
|
11490
|
+
const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : void 0;
|
|
11491
|
+
const name = typeof o.name === "string" ? o.name : void 0;
|
|
11492
|
+
if (id && name) out.push({ id, name });
|
|
11493
|
+
}
|
|
11494
|
+
return out;
|
|
11495
|
+
}
|
|
11496
|
+
async function scanExistingAssets(client, locationId2) {
|
|
11497
|
+
const warnings = [];
|
|
11498
|
+
const assets = {};
|
|
11499
|
+
const read = async (label, fn, into) => {
|
|
11500
|
+
try {
|
|
11501
|
+
assets[into] = await fn();
|
|
11502
|
+
} catch (e) {
|
|
11503
|
+
warnings.push(`could not scan ${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
11504
|
+
}
|
|
11505
|
+
};
|
|
11506
|
+
await read("pipelines", async () => pickObjects(await client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }), ["pipelines"]), "pipelines");
|
|
11507
|
+
await read("custom fields", async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]), "customFields");
|
|
11508
|
+
await read("tags", async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]), "tags");
|
|
11509
|
+
await read("custom values", async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]), "customValues");
|
|
11510
|
+
await read("calendars", async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]), "calendars");
|
|
11511
|
+
await read("forms", async () => pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } }), ["forms"]), "forms");
|
|
11512
|
+
await read("funnels", async () => pickObjects(await client.get("/funnels/funnel/list", { params: { locationId: locationId2, limit: 100 } }), ["funnels"]), "funnels");
|
|
11513
|
+
await read("workflows", async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]), "workflows");
|
|
11514
|
+
return { assets, warnings };
|
|
11515
|
+
}
|
|
11516
|
+
function pickPipelines(raw) {
|
|
11517
|
+
const root = raw && typeof raw === "object" ? raw : {};
|
|
11518
|
+
const list = Array.isArray(root.pipelines) ? root.pipelines : Array.isArray(raw) ? raw : [];
|
|
11519
|
+
const out = [];
|
|
11520
|
+
for (const item of list) {
|
|
11521
|
+
if (!item || typeof item !== "object") continue;
|
|
11522
|
+
const p = item;
|
|
11523
|
+
const id = typeof p.id === "string" ? p.id : typeof p._id === "string" ? p._id : void 0;
|
|
11524
|
+
const name = typeof p.name === "string" ? p.name : void 0;
|
|
11525
|
+
if (!id || !name) continue;
|
|
11526
|
+
const stagesRaw = Array.isArray(p.stages) ? p.stages : [];
|
|
11527
|
+
const stages = stagesRaw.filter((s) => !!s && typeof s === "object").map((s) => ({
|
|
11528
|
+
id: typeof s.id === "string" ? s.id : typeof s._id === "string" ? s._id : "",
|
|
11529
|
+
name: typeof s.name === "string" ? s.name : "",
|
|
11530
|
+
position: typeof s.position === "number" ? s.position : void 0
|
|
11531
|
+
})).filter((s) => s.id && s.name);
|
|
11532
|
+
out.push({ id, name, stages });
|
|
11533
|
+
}
|
|
11534
|
+
return out;
|
|
11535
|
+
}
|
|
11536
|
+
function makeExecuteDeps(client, builderClient, locationId2) {
|
|
11537
|
+
const pipelineApi = async (method, path7, body) => {
|
|
11538
|
+
const headers = await builderClient.buildHeaders();
|
|
11539
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path7}`;
|
|
11540
|
+
const options = { method, headers };
|
|
11541
|
+
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
11542
|
+
const response = await fetch(url, options);
|
|
11543
|
+
if (!response.ok) {
|
|
11544
|
+
const text2 = await response.text();
|
|
11545
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path7}
|
|
11546
|
+
${text2.slice(0, 300)}`);
|
|
11547
|
+
}
|
|
11548
|
+
const text = await response.text();
|
|
11549
|
+
if (!text) return {};
|
|
11550
|
+
try {
|
|
11551
|
+
return JSON.parse(text);
|
|
11552
|
+
} catch {
|
|
11553
|
+
return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
|
|
11554
|
+
}
|
|
11555
|
+
};
|
|
11556
|
+
return {
|
|
11557
|
+
listPipelines: async () => pickPipelines(await pipelineApi("GET", `?locationId=${locationId2}`)),
|
|
11558
|
+
createPipeline: async (name, stages) => {
|
|
11559
|
+
await pipelineApi("POST", "", {
|
|
11560
|
+
name,
|
|
11561
|
+
stages: stages.map((s) => ({ name: s.name, position: s.position, showInFunnel: true, showInPieChart: true })),
|
|
11562
|
+
locationId: locationId2,
|
|
11563
|
+
showInFunnel: true,
|
|
11564
|
+
showInPieChart: true
|
|
11565
|
+
});
|
|
11566
|
+
},
|
|
11567
|
+
// noRetry on every CREATE: these POSTs are not idempotent, so an auto-retry
|
|
11568
|
+
// after a lost response (429/5xx/network) would DUPLICATE the object in the
|
|
11569
|
+
// live account. With noRetry, a failed create simply fails → the executor's
|
|
11570
|
+
// verify-after catches it and halts; the idempotent re-run then binds the
|
|
11571
|
+
// one real object instead of stacking a second.
|
|
11572
|
+
listCustomFields: async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]),
|
|
11573
|
+
createCustomField: async (f) => {
|
|
11574
|
+
const body = { name: f.name, dataType: f.dataType, model: f.model ?? "contact" };
|
|
11575
|
+
if (f.options && f.options.length) body.options = f.options;
|
|
11576
|
+
await client.post(`/locations/${locationId2}/customFields`, { body, noRetry: true });
|
|
11577
|
+
},
|
|
11578
|
+
listTags: async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]),
|
|
11579
|
+
createTag: async (name) => {
|
|
11580
|
+
await client.post(`/locations/${locationId2}/tags`, { body: { name }, noRetry: true });
|
|
11581
|
+
},
|
|
11582
|
+
listCustomValues: async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]),
|
|
11583
|
+
createCustomValue: async (name, value) => {
|
|
11584
|
+
await client.post(`/locations/${locationId2}/customValues`, { body: { name, value }, noRetry: true });
|
|
11585
|
+
}
|
|
11586
|
+
};
|
|
11587
|
+
}
|
|
11588
|
+
async function readLocationName(client, locationId2) {
|
|
11589
|
+
try {
|
|
11590
|
+
const raw = await client.get(`/locations/${locationId2}`);
|
|
11591
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
11592
|
+
const loc = r.location && typeof r.location === "object" ? r.location : r;
|
|
11593
|
+
const name = loc.name;
|
|
11594
|
+
return typeof name === "string" && name ? name : locationId2;
|
|
11595
|
+
} catch {
|
|
11596
|
+
return locationId2;
|
|
11597
|
+
}
|
|
10882
11598
|
}
|
|
10883
11599
|
function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
10884
11600
|
safeTool(
|
|
@@ -10922,6 +11638,145 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
10922
11638
|
},
|
|
10923
11639
|
async ({ plan }) => validateBuildPlan(plan)
|
|
10924
11640
|
);
|
|
11641
|
+
server2.tool(
|
|
11642
|
+
"apply_build_plan",
|
|
11643
|
+
`Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values): never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Calendars/forms/funnels/workflows are surfaced as manual next steps, not auto-built yet. Always confirms the active location and validates the plan before any write.`,
|
|
11644
|
+
{
|
|
11645
|
+
plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
11646
|
+
mode: import_zod53.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
11647
|
+
locationId: import_zod53.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
|
|
11648
|
+
metHandoffs: import_zod53.z.array(import_zod53.z.string()).optional().describe('Handoff refs the operator has already satisfied (e.g. ["handoff.a2p"]) \u2014 lifts their gate so dependent workflows are not held DRAFT.'),
|
|
11649
|
+
publishWorkflows: import_zod53.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
|
|
11650
|
+
onConflict: import_zod53.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
|
|
11651
|
+
},
|
|
11652
|
+
async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
|
|
11653
|
+
try {
|
|
11654
|
+
const resolvedMode = mode ?? "dry_run";
|
|
11655
|
+
const activeLocation = client.resolveLocationId();
|
|
11656
|
+
if (locationId2 && locationId2 !== activeLocation) {
|
|
11657
|
+
return jsonResponse({
|
|
11658
|
+
ok: false,
|
|
11659
|
+
error: `Location mismatch: active sub-account is ${activeLocation} but you passed ${locationId2}. Run switch_location to ${locationId2} and confirm with get_current_location first, then re-run. (Blueprint never switches accounts underneath you.)`
|
|
11660
|
+
});
|
|
11661
|
+
}
|
|
11662
|
+
const validation = validateBuildPlan(plan);
|
|
11663
|
+
if (!validation.valid || !validation.plan) {
|
|
11664
|
+
return jsonResponse({
|
|
11665
|
+
ok: false,
|
|
11666
|
+
phase: "validate",
|
|
11667
|
+
error: "Build plan failed \xA75 validation \u2014 fix these before building.",
|
|
11668
|
+
errors: validation.errors,
|
|
11669
|
+
warnings: validation.warnings
|
|
11670
|
+
});
|
|
11671
|
+
}
|
|
11672
|
+
const typedPlan = validation.plan;
|
|
11673
|
+
const locationName = await readLocationName(client, activeLocation);
|
|
11674
|
+
const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation);
|
|
11675
|
+
const result = resolvePlan(typedPlan, assets, { metHandoffs });
|
|
11676
|
+
if (resolvedMode === "execute") {
|
|
11677
|
+
if (!builderClient) {
|
|
11678
|
+
return jsonResponse({
|
|
11679
|
+
ok: false,
|
|
11680
|
+
phase: "execute",
|
|
11681
|
+
error: "execute needs the workflow-builder (Firebase) client to create pipelines, and it is not configured on this install. Run the setup wizard to enable the builder, then retry. dry_run works without it."
|
|
11682
|
+
});
|
|
11683
|
+
}
|
|
11684
|
+
if (builderClient.locationId && builderClient.locationId !== activeLocation) {
|
|
11685
|
+
return jsonResponse({
|
|
11686
|
+
ok: false,
|
|
11687
|
+
phase: "execute",
|
|
11688
|
+
error: `Location mismatch: the public API is on ${activeLocation} but the workflow-builder client is on ${builderClient.locationId}. Restart Claude or run switch_location so both point at the same sub-account before building (registry-staleness guard). No writes were made.`
|
|
11689
|
+
});
|
|
11690
|
+
}
|
|
11691
|
+
try {
|
|
11692
|
+
await builderClient.buildHeaders();
|
|
11693
|
+
} catch (e) {
|
|
11694
|
+
return jsonResponse({ ok: false, phase: "execute", error: `Could not authenticate the workflow-builder for ${activeLocation} (${e instanceof Error ? e.message : String(e)}). No writes were made.` });
|
|
11695
|
+
}
|
|
11696
|
+
const tokenCompany = builderClient.getTokenCompanyId();
|
|
11697
|
+
const intendedCompany = builderClient.getIntendedCompanyId();
|
|
11698
|
+
if (tokenCompany && intendedCompany && tokenCompany !== intendedCompany) {
|
|
11699
|
+
return jsonResponse({
|
|
11700
|
+
ok: false,
|
|
11701
|
+
phase: "execute",
|
|
11702
|
+
error: `Firebase binding mismatch: the workflow-builder authenticates as company ${tokenCompany} but the active location ${activeLocation} is owned by company ${intendedCompany}. Restart Claude or re-run switch_location to ${activeLocation} so the Firebase session rebinds before building. No writes were made.`
|
|
11703
|
+
});
|
|
11704
|
+
}
|
|
11705
|
+
if (onConflict === "abort") {
|
|
11706
|
+
const collisions2 = result.items.filter((i) => i.status === "existing");
|
|
11707
|
+
if (collisions2.length > 0) {
|
|
11708
|
+
return jsonResponse({
|
|
11709
|
+
ok: false,
|
|
11710
|
+
phase: "execute",
|
|
11711
|
+
error: `onConflict=abort: ${collisions2.length} object(s) in the plan already exist in this account \u2014 aborting before any write. Re-run with onConflict:"skip" to bind to the existing ones, or resolve them first.`,
|
|
11712
|
+
collisions: collisions2.map((c) => ({ ref: c.ref, name: c.name, existingId: c.existingId }))
|
|
11713
|
+
});
|
|
11714
|
+
}
|
|
11715
|
+
}
|
|
11716
|
+
const deps = makeExecuteDeps(client, builderClient, activeLocation);
|
|
11717
|
+
const exec = await executeBackbone(typedPlan, deps);
|
|
11718
|
+
const manualLines = result.workflows.flatMap((w) => [
|
|
11719
|
+
...w.manual.map((m) => `[${w.name}] ${m.reason}`),
|
|
11720
|
+
...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
|
|
11721
|
+
]);
|
|
11722
|
+
const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
|
|
11723
|
+
return jsonResponse({
|
|
11724
|
+
ok: exec.ok,
|
|
11725
|
+
mode: "execute",
|
|
11726
|
+
locationId: activeLocation,
|
|
11727
|
+
locationName,
|
|
11728
|
+
planId: typedPlan.planId,
|
|
11729
|
+
scanWarnings,
|
|
11730
|
+
halted: exec.halted,
|
|
11731
|
+
built: exec.built,
|
|
11732
|
+
idMap: exec.idMap,
|
|
11733
|
+
deferred: exec.deferred,
|
|
11734
|
+
deferredNote: "v1 execute builds the CRM backbone (pipelines, custom fields, tags, custom values) live. These object types are planned but NOT auto-built yet \u2014 create them via the GHL UI or the dedicated tools, in this order: calendars \u2192 forms \u2192 funnels \u2192 workflows.",
|
|
11735
|
+
nextManualSteps: [...manualLines, ...handoffLines],
|
|
11736
|
+
summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing").length} existing. ${exec.deferred.length ? "Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
|
|
11737
|
+
});
|
|
11738
|
+
}
|
|
11739
|
+
const collisions = result.items.filter((i) => i.status === "existing");
|
|
11740
|
+
const aborted = onConflict === "abort" && collisions.length > 0;
|
|
11741
|
+
const report = renderReport(typedPlan, result, {
|
|
11742
|
+
mode: "dry_run",
|
|
11743
|
+
locationName,
|
|
11744
|
+
locationId: activeLocation,
|
|
11745
|
+
publishWorkflows: publishWorkflows ?? false
|
|
11746
|
+
});
|
|
11747
|
+
return jsonResponse({
|
|
11748
|
+
ok: true,
|
|
11749
|
+
mode: "dry_run",
|
|
11750
|
+
locationId: activeLocation,
|
|
11751
|
+
locationName,
|
|
11752
|
+
planId: typedPlan.planId,
|
|
11753
|
+
validation: { valid: true, warnings: validation.warnings, referencesScanned: validation.referencesScanned },
|
|
11754
|
+
scanWarnings,
|
|
11755
|
+
onConflict: onConflict ?? "skip",
|
|
11756
|
+
aborted: aborted ? { reason: `${collisions.length} same-named object(s) already exist and onConflict=abort`, collisions: collisions.map((c) => c.ref) } : void 0,
|
|
11757
|
+
summary: result.summary,
|
|
11758
|
+
items: result.items,
|
|
11759
|
+
idMap: result.idMap,
|
|
11760
|
+
workflows: result.workflows.map((w) => ({
|
|
11761
|
+
ref: w.ref,
|
|
11762
|
+
name: w.name,
|
|
11763
|
+
autoActions: w.nativeActions.length,
|
|
11764
|
+
manualSteps: w.manual,
|
|
11765
|
+
needsContent: w.needsContent,
|
|
11766
|
+
pendingRefs: w.pendingRefs,
|
|
11767
|
+
splitInto: w.splitInto,
|
|
11768
|
+
gatedBy: w.gatedBy,
|
|
11769
|
+
draft: w.gatedBy.length > 0 || !(publishWorkflows ?? false)
|
|
11770
|
+
})),
|
|
11771
|
+
handoffs: result.handoffs,
|
|
11772
|
+
report,
|
|
11773
|
+
next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone live (pipelines, fields, tags, custom values). Calendars/forms/funnels/workflows are listed as manual next steps.'
|
|
11774
|
+
});
|
|
11775
|
+
} catch (error) {
|
|
11776
|
+
return errorResponse(error);
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
);
|
|
10925
11780
|
if (!builderClient) return;
|
|
10926
11781
|
const bc = builderClient;
|
|
10927
11782
|
async function resolveCustomFields(locationId2, dryRun) {
|
|
@@ -10967,7 +11822,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
10967
11822
|
resolved.set(q.key, rec);
|
|
10968
11823
|
}
|
|
10969
11824
|
if (!missing) break;
|
|
10970
|
-
if (attempt < 6) await
|
|
11825
|
+
if (attempt < 6) await sleep2(700 * attempt);
|
|
10971
11826
|
}
|
|
10972
11827
|
if (missing) {
|
|
10973
11828
|
throw new Error(
|
|
@@ -11033,7 +11888,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11033
11888
|
break;
|
|
11034
11889
|
} catch (saveErr) {
|
|
11035
11890
|
if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
|
|
11036
|
-
await
|
|
11891
|
+
await sleep2(700 * attempt);
|
|
11037
11892
|
continue;
|
|
11038
11893
|
}
|
|
11039
11894
|
throw saveErr;
|
|
@@ -11045,7 +11900,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11045
11900
|
const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
|
|
11046
11901
|
persistedCount = countFormFields(verify);
|
|
11047
11902
|
if (persistedCount > 0) break;
|
|
11048
|
-
if (attempt < 6) await
|
|
11903
|
+
if (attempt < 6) await sleep2(700 * attempt);
|
|
11049
11904
|
}
|
|
11050
11905
|
const fieldMap = {};
|
|
11051
11906
|
for (const [key, rec] of resolved) fieldMap[key] = rec.id;
|
|
@@ -11367,8 +12222,8 @@ Subcommands:
|
|
|
11367
12222
|
|
|
11368
12223
|
Exit codes: 0 ok, 2 usage, 3 validation failed, 4 filesystem write failed.
|
|
11369
12224
|
Seed while the MCP server is stopped, or restart it afterwards.`;
|
|
11370
|
-
function errLine(
|
|
11371
|
-
process.stderr.write(
|
|
12225
|
+
function errLine(msg2) {
|
|
12226
|
+
process.stderr.write(msg2 + "\n");
|
|
11372
12227
|
}
|
|
11373
12228
|
function preflightWritable() {
|
|
11374
12229
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 218 tools — full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|