@elitedcs/ghl-mcp 3.37.0 → 3.39.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 +393 -13
- 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.39.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",
|
|
@@ -9625,8 +9625,8 @@ var import_zod53 = require("zod");
|
|
|
9625
9625
|
var QUESTION_SET_VERSION = "0.1";
|
|
9626
9626
|
var INTAKE_FIELD_NAME_PREFIX = "Intake: ";
|
|
9627
9627
|
function deriveFieldKey(fieldName) {
|
|
9628
|
-
const
|
|
9629
|
-
return `contact.${
|
|
9628
|
+
const slug3 = fieldName.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/ /g, "_");
|
|
9629
|
+
return `contact.${slug3}`;
|
|
9630
9630
|
}
|
|
9631
9631
|
function intakeFieldName(label) {
|
|
9632
9632
|
return `${INTAKE_FIELD_NAME_PREFIX}${label}`;
|
|
@@ -10225,6 +10225,101 @@ function buildIntakeFormData(opts) {
|
|
|
10225
10225
|
};
|
|
10226
10226
|
}
|
|
10227
10227
|
|
|
10228
|
+
// src/intake-to-build/plan-form.ts
|
|
10229
|
+
function slug2(s) {
|
|
10230
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("_");
|
|
10231
|
+
}
|
|
10232
|
+
function humanizeKey(key) {
|
|
10233
|
+
return key.split(/[^a-z0-9]+/i).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
10234
|
+
}
|
|
10235
|
+
function buildPlanStandardField(key, required) {
|
|
10236
|
+
const isEmail = key === "email";
|
|
10237
|
+
const isPhone = key === "phone";
|
|
10238
|
+
const label = humanizeKey(key);
|
|
10239
|
+
const base = {
|
|
10240
|
+
fieldWidthPercentage: 100,
|
|
10241
|
+
hiddenFieldQueryKey: key,
|
|
10242
|
+
label,
|
|
10243
|
+
placeholder: label,
|
|
10244
|
+
required,
|
|
10245
|
+
standard: true,
|
|
10246
|
+
tag: key,
|
|
10247
|
+
type: isEmail ? "email" : "text",
|
|
10248
|
+
typeLabel: isEmail ? "Email" : isPhone ? "Phone" : "Text"
|
|
10249
|
+
};
|
|
10250
|
+
if (isPhone) base.enableCountryPicker = false;
|
|
10251
|
+
return base;
|
|
10252
|
+
}
|
|
10253
|
+
function buildPlanCustomField(record, required, locationId2, position) {
|
|
10254
|
+
const type = formFieldType(record.dataType);
|
|
10255
|
+
const field = {
|
|
10256
|
+
Id: record.id,
|
|
10257
|
+
id: record.id,
|
|
10258
|
+
tag: record.id,
|
|
10259
|
+
active: false,
|
|
10260
|
+
allowCustomOption: false,
|
|
10261
|
+
customFieldLabel: record.name,
|
|
10262
|
+
dataType: record.dataType,
|
|
10263
|
+
dateAdded: record.dateAdded ?? "",
|
|
10264
|
+
description: "",
|
|
10265
|
+
documentType: "field",
|
|
10266
|
+
edit: false,
|
|
10267
|
+
fieldKey: record.fieldKey,
|
|
10268
|
+
fieldWidthPercentage: 100,
|
|
10269
|
+
fieldsCount: 0,
|
|
10270
|
+
hiddenFieldQueryKey: slug2(record.name),
|
|
10271
|
+
label: record.name,
|
|
10272
|
+
locationId: locationId2,
|
|
10273
|
+
model: record.model ?? "contact",
|
|
10274
|
+
name: record.name,
|
|
10275
|
+
parentId: record.parentId ?? "",
|
|
10276
|
+
placeholder: "",
|
|
10277
|
+
position,
|
|
10278
|
+
required,
|
|
10279
|
+
showInForms: true,
|
|
10280
|
+
standard: false,
|
|
10281
|
+
type
|
|
10282
|
+
};
|
|
10283
|
+
const options = record.picklistOptions ?? void 0;
|
|
10284
|
+
if (type === "single_options" || type === "multiple_options" || type === "checkbox") {
|
|
10285
|
+
field.picklistOptions = options ? [...options] : [];
|
|
10286
|
+
}
|
|
10287
|
+
if (type === "multiple_options") {
|
|
10288
|
+
field.calculatedOptions = (options ?? []).map((label) => ({ calculatedValue: "", label }));
|
|
10289
|
+
field.category = "choiceElements";
|
|
10290
|
+
field.typeLabel = "Multi Dropdown";
|
|
10291
|
+
}
|
|
10292
|
+
if (type === "phone") field.enableCountryPicker = false;
|
|
10293
|
+
return field;
|
|
10294
|
+
}
|
|
10295
|
+
function buildPlanFormData(fields, locationId2) {
|
|
10296
|
+
const out = [];
|
|
10297
|
+
let position = 0;
|
|
10298
|
+
for (const f of fields) {
|
|
10299
|
+
if (f.kind === "standard") {
|
|
10300
|
+
out.push(buildPlanStandardField(f.key, f.required));
|
|
10301
|
+
} else {
|
|
10302
|
+
out.push(buildPlanCustomField(f.record, f.required, locationId2, position += 50));
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
out.push(buildSubmitButton("Submit"));
|
|
10306
|
+
return {
|
|
10307
|
+
autoResponder: false,
|
|
10308
|
+
emailNotifications: false,
|
|
10309
|
+
form: {
|
|
10310
|
+
fields: out,
|
|
10311
|
+
formLabelVisible: true,
|
|
10312
|
+
formAction: {
|
|
10313
|
+
actionType: "2",
|
|
10314
|
+
headerImageSrc: "",
|
|
10315
|
+
mobileHeaderImageSrc: "",
|
|
10316
|
+
redirectUrl: "",
|
|
10317
|
+
thankyouText: "<p style='text-align:center;margin:0;'>Thanks! We received your submission.</p>"
|
|
10318
|
+
}
|
|
10319
|
+
}
|
|
10320
|
+
};
|
|
10321
|
+
}
|
|
10322
|
+
|
|
10228
10323
|
// src/intake-to-build/brief.ts
|
|
10229
10324
|
var import_zod51 = require("zod");
|
|
10230
10325
|
var BRIEF_SCHEMA_VERSION = "0.1";
|
|
@@ -10777,6 +10872,26 @@ function checkRefIntegrity(plan, defined) {
|
|
|
10777
10872
|
for (const b of plan.buildOrder ?? []) checkMaybeWildcard(b, "buildOrder");
|
|
10778
10873
|
return { errors, scanned };
|
|
10779
10874
|
}
|
|
10875
|
+
var KNOWN_STANDARD_FORM_KEYS = /* @__PURE__ */ new Set([
|
|
10876
|
+
"first_name",
|
|
10877
|
+
"last_name",
|
|
10878
|
+
"name",
|
|
10879
|
+
"full_name",
|
|
10880
|
+
"email",
|
|
10881
|
+
"phone",
|
|
10882
|
+
"address1",
|
|
10883
|
+
"address",
|
|
10884
|
+
"city",
|
|
10885
|
+
"state",
|
|
10886
|
+
"postal_code",
|
|
10887
|
+
"country",
|
|
10888
|
+
"website",
|
|
10889
|
+
"organization",
|
|
10890
|
+
"company_name",
|
|
10891
|
+
"date_of_birth",
|
|
10892
|
+
"contact_source",
|
|
10893
|
+
"source"
|
|
10894
|
+
]);
|
|
10780
10895
|
function validateBuildPlan(input) {
|
|
10781
10896
|
const parsed = buildPlanSchema.safeParse(input);
|
|
10782
10897
|
if (!parsed.success) {
|
|
@@ -10806,7 +10921,39 @@ function validateBuildPlan(input) {
|
|
|
10806
10921
|
seen.add(key);
|
|
10807
10922
|
}
|
|
10808
10923
|
}
|
|
10924
|
+
const nameGroups = [
|
|
10925
|
+
["pipelines", (plan.pipelines ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10926
|
+
["customFields", (plan.customFields ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10927
|
+
["tags", (plan.tags ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10928
|
+
["customValues", (plan.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10929
|
+
["calendars", (plan.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10930
|
+
["forms", (plan.forms ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10931
|
+
["funnels", (plan.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name }))]
|
|
10932
|
+
];
|
|
10933
|
+
for (const [type, objs] of nameGroups) {
|
|
10934
|
+
const seen = /* @__PURE__ */ new Map();
|
|
10935
|
+
for (const o of objs) {
|
|
10936
|
+
const key = o.name.trim().toLowerCase();
|
|
10937
|
+
const prev = seen.get(key);
|
|
10938
|
+
if (prev) {
|
|
10939
|
+
allErrors.push(
|
|
10940
|
+
`${type} "${o.ref}" and "${prev}" share the name "${o.name}" \u2014 names must be unique within a type (the executor binds same-named objects by name, so duplicates would collapse onto one real id)`
|
|
10941
|
+
);
|
|
10942
|
+
} else {
|
|
10943
|
+
seen.set(key, o.ref);
|
|
10944
|
+
}
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10809
10947
|
const warnings = [];
|
|
10948
|
+
for (const fm of plan.forms ?? []) {
|
|
10949
|
+
for (const fl of fm.fields) {
|
|
10950
|
+
if (fl.type === "standard" && !KNOWN_STANDARD_FORM_KEYS.has(fl.key.trim().toLowerCase())) {
|
|
10951
|
+
warnings.push(
|
|
10952
|
+
`forms "${fm.ref}" standard field key "${fl.key}" is not a recognized GHL standard contact field \u2014 GHL may not save it; if this is custom data, define a custom field and reference it with a custom fieldRef`
|
|
10953
|
+
);
|
|
10954
|
+
}
|
|
10955
|
+
}
|
|
10956
|
+
}
|
|
10810
10957
|
for (const p of plan.pipelines ?? []) {
|
|
10811
10958
|
const positions = p.stages.map((s) => s.position).sort((a, b) => a - b);
|
|
10812
10959
|
const expected = positions.every((pos, idx) => pos === idx);
|
|
@@ -10851,6 +10998,35 @@ var WAIT_UNIT_MAP = {
|
|
|
10851
10998
|
};
|
|
10852
10999
|
var PENDING = (ref) => `__PENDING__:${ref}`;
|
|
10853
11000
|
var isPending = (v) => v.startsWith("__PENDING__:");
|
|
11001
|
+
var STAFF_CALENDAR_TYPES = /* @__PURE__ */ new Set([
|
|
11002
|
+
"round_robin",
|
|
11003
|
+
"collective",
|
|
11004
|
+
"class_booking",
|
|
11005
|
+
"service_booking"
|
|
11006
|
+
]);
|
|
11007
|
+
function calendarNeedsStaff(cal) {
|
|
11008
|
+
return cal.requiresStaff === true || STAFF_CALENDAR_TYPES.has(cal.calendarType);
|
|
11009
|
+
}
|
|
11010
|
+
function classifyCalendarBuild(cal, userCount) {
|
|
11011
|
+
if (!calendarNeedsStaff(cal)) return { action: "build" };
|
|
11012
|
+
if (userCount === 1) return { action: "build" };
|
|
11013
|
+
if (userCount === void 0) {
|
|
11014
|
+
return {
|
|
11015
|
+
action: "manual",
|
|
11016
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, but Blueprint couldn't read this account's users to auto-assign one. Assign the booking staff to it in the GHL UI (or build it there), then re-run to bind it.`
|
|
11017
|
+
};
|
|
11018
|
+
}
|
|
11019
|
+
if (userCount === 0) {
|
|
11020
|
+
return {
|
|
11021
|
+
action: "manual",
|
|
11022
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, but this account has no users yet. Add a user and assign them to it, then re-run to build it.`
|
|
11023
|
+
};
|
|
11024
|
+
}
|
|
11025
|
+
return {
|
|
11026
|
+
action: "manual",
|
|
11027
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, and this account has ${userCount} users \u2014 Blueprint won't guess which one books. Assign the booking staff to it in the GHL UI (or build it there), then re-run to bind it.`
|
|
11028
|
+
};
|
|
11029
|
+
}
|
|
10854
11030
|
var norm = (s) => s.trim().toLowerCase();
|
|
10855
11031
|
function buildRefIndex(plan) {
|
|
10856
11032
|
const idx = {
|
|
@@ -11192,6 +11368,15 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11192
11368
|
}
|
|
11193
11369
|
}
|
|
11194
11370
|
}
|
|
11371
|
+
const existingCalNames = new Set((existing.calendars ?? []).map((c) => norm(c.name)));
|
|
11372
|
+
const calendarsManual = [];
|
|
11373
|
+
for (const cal of plan.calendars ?? []) {
|
|
11374
|
+
if (existingCalNames.has(norm(cal.name))) continue;
|
|
11375
|
+
const decision = classifyCalendarBuild(cal, existing.userCount);
|
|
11376
|
+
if (decision.action === "manual") {
|
|
11377
|
+
calendarsManual.push({ ref: cal.ref, name: cal.name, reason: decision.reason ?? "needs a team member" });
|
|
11378
|
+
}
|
|
11379
|
+
}
|
|
11195
11380
|
const gates = computeWorkflowGates(plan, metHandoffs);
|
|
11196
11381
|
const workflows = (plan.workflows ?? []).map((w) => expandWorkflow(w, idx, idMap, gates.get(w.ref) ?? []));
|
|
11197
11382
|
const handoffs = (plan.handoffs ?? []).map((h) => ({
|
|
@@ -11203,8 +11388,9 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11203
11388
|
met: metHandoffs.has(h.ref),
|
|
11204
11389
|
blocks: h.blocks ?? []
|
|
11205
11390
|
}));
|
|
11391
|
+
const manualCalRefs = new Set(calendarsManual.map((c) => c.ref));
|
|
11206
11392
|
const summary = {
|
|
11207
|
-
wouldCreate: items.filter((i) => i.status === "would_create").length,
|
|
11393
|
+
wouldCreate: items.filter((i) => i.status === "would_create" && !manualCalRefs.has(i.ref)).length,
|
|
11208
11394
|
existing: items.filter((i) => i.status === "existing").length,
|
|
11209
11395
|
workflowsTotal: workflows.length,
|
|
11210
11396
|
workflowsGated: workflows.filter((w) => w.gatedBy.length > 0).length,
|
|
@@ -11212,7 +11398,7 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11212
11398
|
actionsManual: workflows.reduce((n, w) => n + w.manual.length, 0),
|
|
11213
11399
|
actionsNeedContent: workflows.reduce((n, w) => n + w.needsContent.length, 0)
|
|
11214
11400
|
};
|
|
11215
|
-
return { items, idMap: Object.fromEntries(idMap), workflows, handoffs, summary };
|
|
11401
|
+
return { items, idMap: Object.fromEntries(idMap), workflows, calendarsManual, handoffs, summary };
|
|
11216
11402
|
}
|
|
11217
11403
|
function renderReport(plan, result, ctx) {
|
|
11218
11404
|
const L = [];
|
|
@@ -11228,11 +11414,13 @@ function renderReport(plan, result, ctx) {
|
|
|
11228
11414
|
`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
11415
|
);
|
|
11230
11416
|
L.push("");
|
|
11417
|
+
const manualCalRefs = new Set(result.calendarsManual.map((c) => c.ref));
|
|
11231
11418
|
L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
|
|
11232
11419
|
for (const section2 of EXECUTION_ORDER) {
|
|
11233
11420
|
const secItems = result.items.filter((i) => i.type === section2);
|
|
11234
11421
|
if (secItems.length === 0) continue;
|
|
11235
11422
|
for (const it of secItems) {
|
|
11423
|
+
if (manualCalRefs.has(it.ref)) continue;
|
|
11236
11424
|
const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
|
|
11237
11425
|
L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
|
|
11238
11426
|
}
|
|
@@ -11245,6 +11433,10 @@ function renderReport(plan, result, ctx) {
|
|
|
11245
11433
|
L.push("");
|
|
11246
11434
|
L.push("\u2500\u2500 You must do these by hand (in order) \u2500\u2500");
|
|
11247
11435
|
let any = false;
|
|
11436
|
+
for (const c of result.calendarsManual) {
|
|
11437
|
+
any = true;
|
|
11438
|
+
L.push(` \u2022 [calendar] ${c.reason}`);
|
|
11439
|
+
}
|
|
11248
11440
|
for (const w of result.workflows) {
|
|
11249
11441
|
for (const m of w.manual) {
|
|
11250
11442
|
any = true;
|
|
@@ -11272,10 +11464,12 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
11272
11464
|
const backoff = opts.verifyBackoffMs ?? 500;
|
|
11273
11465
|
const idMap = {};
|
|
11274
11466
|
const built = [];
|
|
11467
|
+
const manual = [];
|
|
11275
11468
|
const halt = (atRef, type, reason) => ({
|
|
11276
11469
|
ok: false,
|
|
11277
11470
|
idMap,
|
|
11278
11471
|
built,
|
|
11472
|
+
manual,
|
|
11279
11473
|
halted: { atRef, type, reason },
|
|
11280
11474
|
deferred: deferredSections(plan)
|
|
11281
11475
|
});
|
|
@@ -11363,7 +11557,117 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
11363
11557
|
built
|
|
11364
11558
|
);
|
|
11365
11559
|
if (cvHalt) return halt(cvHalt.ref, "cv", cvHalt.reason);
|
|
11366
|
-
|
|
11560
|
+
let cachedUserCount;
|
|
11561
|
+
let cachedSoloUserId;
|
|
11562
|
+
let usersRead = false;
|
|
11563
|
+
for (const cal of plan.calendars ?? []) {
|
|
11564
|
+
let calendars;
|
|
11565
|
+
try {
|
|
11566
|
+
calendars = await deps.listCalendars();
|
|
11567
|
+
} catch (e) {
|
|
11568
|
+
return halt(cal.ref, "calendar", `could not read existing calendars: ${msg(e)}`);
|
|
11569
|
+
}
|
|
11570
|
+
const matches = calendars.filter((c) => norm2(c.name) === norm2(cal.name));
|
|
11571
|
+
if (matches.length > 1) {
|
|
11572
|
+
return halt(cal.ref, "calendar", `${matches.length} existing calendars are named "${cal.name}" \u2014 ambiguous, cannot safely bind ${cal.ref}. Resolve the duplicate in GHL, then re-run.`);
|
|
11573
|
+
}
|
|
11574
|
+
if (matches.length === 1) {
|
|
11575
|
+
idMap[cal.ref] = matches[0].id;
|
|
11576
|
+
built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "existing", realId: matches[0].id });
|
|
11577
|
+
continue;
|
|
11578
|
+
}
|
|
11579
|
+
let staffUserId;
|
|
11580
|
+
if (calendarNeedsStaff(cal)) {
|
|
11581
|
+
if (!usersRead) {
|
|
11582
|
+
try {
|
|
11583
|
+
const users = await deps.listUsers();
|
|
11584
|
+
cachedUserCount = users.length;
|
|
11585
|
+
cachedSoloUserId = users.length === 1 ? users[0].id : void 0;
|
|
11586
|
+
} catch {
|
|
11587
|
+
cachedUserCount = void 0;
|
|
11588
|
+
}
|
|
11589
|
+
usersRead = true;
|
|
11590
|
+
}
|
|
11591
|
+
const decision = classifyCalendarBuild(cal, cachedUserCount);
|
|
11592
|
+
if (decision.action === "manual") {
|
|
11593
|
+
manual.push({ ref: cal.ref, type: "calendar", name: cal.name, reason: decision.reason ?? "needs a team member" });
|
|
11594
|
+
continue;
|
|
11595
|
+
}
|
|
11596
|
+
staffUserId = cachedSoloUserId;
|
|
11597
|
+
}
|
|
11598
|
+
const beforeIds = new Set(calendars.map((c) => c.id));
|
|
11599
|
+
try {
|
|
11600
|
+
await deps.createCalendar({
|
|
11601
|
+
name: cal.name,
|
|
11602
|
+
calendarType: cal.calendarType,
|
|
11603
|
+
openHours: cal.openHours,
|
|
11604
|
+
availabilityType: cal.availabilityType,
|
|
11605
|
+
staffUserId
|
|
11606
|
+
});
|
|
11607
|
+
} catch (e) {
|
|
11608
|
+
return halt(cal.ref, "calendar", `create failed: ${msg(e)}`);
|
|
11609
|
+
}
|
|
11610
|
+
const verified = await pollForNew(() => deps.listCalendars(), cal.name, beforeIds);
|
|
11611
|
+
if (!verified) return halt(cal.ref, "calendar", "created but could not verify a single new calendar by read-back (none, or an ambiguous duplicate, appeared)");
|
|
11612
|
+
idMap[cal.ref] = verified.id;
|
|
11613
|
+
built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "created", realId: verified.id });
|
|
11614
|
+
}
|
|
11615
|
+
let cfRecords;
|
|
11616
|
+
for (const form of plan.forms ?? []) {
|
|
11617
|
+
let forms;
|
|
11618
|
+
try {
|
|
11619
|
+
forms = await deps.listForms();
|
|
11620
|
+
} catch (e) {
|
|
11621
|
+
return halt(form.ref, "form", `could not read existing forms: ${msg(e)}`);
|
|
11622
|
+
}
|
|
11623
|
+
const matches = forms.filter((f) => norm2(f.name) === norm2(form.name));
|
|
11624
|
+
if (matches.length > 1) {
|
|
11625
|
+
return halt(form.ref, "form", `${matches.length} existing forms are named "${form.name}" \u2014 ambiguous, cannot safely bind ${form.ref}. Resolve the duplicate in GHL, then re-run.`);
|
|
11626
|
+
}
|
|
11627
|
+
if (matches.length === 1) {
|
|
11628
|
+
idMap[form.ref] = matches[0].id;
|
|
11629
|
+
built.push({ ref: form.ref, type: "form", name: form.name, status: "existing", realId: matches[0].id });
|
|
11630
|
+
continue;
|
|
11631
|
+
}
|
|
11632
|
+
if (!cfRecords) {
|
|
11633
|
+
try {
|
|
11634
|
+
cfRecords = await deps.listCustomFieldRecords();
|
|
11635
|
+
} catch (e) {
|
|
11636
|
+
return halt(form.ref, "form", `could not read custom fields to resolve form refs: ${msg(e)}`);
|
|
11637
|
+
}
|
|
11638
|
+
}
|
|
11639
|
+
const byId = new Map(cfRecords.map((r) => [r.id, r]));
|
|
11640
|
+
const resolved = [];
|
|
11641
|
+
let fieldHalt2;
|
|
11642
|
+
for (const ff of form.fields) {
|
|
11643
|
+
if (ff.type === "standard") {
|
|
11644
|
+
resolved.push({ kind: "standard", key: ff.key, required: ff.required ?? false });
|
|
11645
|
+
continue;
|
|
11646
|
+
}
|
|
11647
|
+
const realId = idMap[ff.fieldRef];
|
|
11648
|
+
if (!realId) {
|
|
11649
|
+
fieldHalt2 = halt(form.ref, "form", `form "${form.name}" references ${ff.fieldRef} but that custom field was not built/resolved \u2014 cannot build the form`);
|
|
11650
|
+
break;
|
|
11651
|
+
}
|
|
11652
|
+
const rec = byId.get(realId);
|
|
11653
|
+
if (!rec) {
|
|
11654
|
+
fieldHalt2 = halt(form.ref, "form", `custom field ${ff.fieldRef} (id ${realId}) not found in this account when building form "${form.name}"`);
|
|
11655
|
+
break;
|
|
11656
|
+
}
|
|
11657
|
+
resolved.push({ kind: "custom", record: rec, required: ff.required ?? false });
|
|
11658
|
+
}
|
|
11659
|
+
if (fieldHalt2) return fieldHalt2;
|
|
11660
|
+
let formId;
|
|
11661
|
+
try {
|
|
11662
|
+
formId = await deps.createForm(form.name, resolved);
|
|
11663
|
+
} catch (e) {
|
|
11664
|
+
return halt(form.ref, "form", `create failed: ${msg(e)}`);
|
|
11665
|
+
}
|
|
11666
|
+
if (!formId) return halt(form.ref, "form", "form create returned no id");
|
|
11667
|
+
idMap[form.ref] = formId;
|
|
11668
|
+
built.push({ ref: form.ref, type: "form", name: form.name, status: "created", realId: formId });
|
|
11669
|
+
}
|
|
11670
|
+
return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
|
|
11367
11671
|
}
|
|
11368
11672
|
async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
|
|
11369
11673
|
if (objects.length === 0) return null;
|
|
@@ -11399,8 +11703,6 @@ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMa
|
|
|
11399
11703
|
}
|
|
11400
11704
|
function deferredSections(plan) {
|
|
11401
11705
|
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
11706
|
if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
|
|
11405
11707
|
if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
|
|
11406
11708
|
return out;
|
|
@@ -11511,6 +11813,11 @@ async function scanExistingAssets(client, locationId2) {
|
|
|
11511
11813
|
await read("forms", async () => pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } }), ["forms"]), "forms");
|
|
11512
11814
|
await read("funnels", async () => pickObjects(await client.get("/funnels/funnel/list", { params: { locationId: locationId2, limit: 100 } }), ["funnels"]), "funnels");
|
|
11513
11815
|
await read("workflows", async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]), "workflows");
|
|
11816
|
+
try {
|
|
11817
|
+
assets.userCount = pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]).length;
|
|
11818
|
+
} catch (e) {
|
|
11819
|
+
warnings.push(`could not scan users (staff-requiring calendars will defer to a manual step): ${e instanceof Error ? e.message : String(e)}`);
|
|
11820
|
+
}
|
|
11514
11821
|
return { assets, warnings };
|
|
11515
11822
|
}
|
|
11516
11823
|
function pickPipelines(raw) {
|
|
@@ -11582,6 +11889,76 @@ ${text2.slice(0, 300)}`);
|
|
|
11582
11889
|
listCustomValues: async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]),
|
|
11583
11890
|
createCustomValue: async (name, value) => {
|
|
11584
11891
|
await client.post(`/locations/${locationId2}/customValues`, { body: { name, value }, noRetry: true });
|
|
11892
|
+
},
|
|
11893
|
+
listCalendars: async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]),
|
|
11894
|
+
// Users: count + the lone id (when solo) drive auto-staff. The /users/
|
|
11895
|
+
// endpoint 422s on a `limit` param under PIT auth, so pass only locationId.
|
|
11896
|
+
listUsers: async () => pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]),
|
|
11897
|
+
createCalendar: async (cal) => {
|
|
11898
|
+
const teamMembers = cal.staffUserId ? [{ userId: cal.staffUserId, priority: 1, isPrimary: true, selected: true, locationConfigurations: [{ kind: "custom", position: 0 }] }] : void 0;
|
|
11899
|
+
const body = buildCreateCalendarBody(
|
|
11900
|
+
{ name: cal.name, calendarType: cal.calendarType, openHours: cal.openHours, availabilityType: cal.availabilityType, teamMembers },
|
|
11901
|
+
locationId2
|
|
11902
|
+
);
|
|
11903
|
+
await client.post("/calendars/", { body, noRetry: true });
|
|
11904
|
+
},
|
|
11905
|
+
// Paginate the form list: a single limit:100 page would miss the target (or
|
|
11906
|
+
// an orphan) form on accounts with many forms, breaking NEVER-CLOBBER (the
|
|
11907
|
+
// executor would create a duplicate). Mirrors the update_form name-lookup scan.
|
|
11908
|
+
listForms: async () => {
|
|
11909
|
+
const out = [];
|
|
11910
|
+
const pageSize = 100;
|
|
11911
|
+
const maxForms = 5e3;
|
|
11912
|
+
for (let skip = 0; skip < maxForms; skip += pageSize) {
|
|
11913
|
+
const page = pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: pageSize, skip } }), ["forms"]);
|
|
11914
|
+
out.push(...page);
|
|
11915
|
+
if (page.length < pageSize) break;
|
|
11916
|
+
}
|
|
11917
|
+
return out;
|
|
11918
|
+
},
|
|
11919
|
+
listCustomFieldRecords: async () => parseCustomFields(await client.get(`/locations/${locationId2}/customFields`)),
|
|
11920
|
+
createForm: async (name, fields) => {
|
|
11921
|
+
const formData = buildPlanFormData(fields, locationId2);
|
|
11922
|
+
const createResult = await formApiRequest(builderClient, "POST", `/?locationId=${locationId2}`, {
|
|
11923
|
+
name,
|
|
11924
|
+
locationId: locationId2,
|
|
11925
|
+
formData: { form: { fields: [], formLabelVisible: true } }
|
|
11926
|
+
});
|
|
11927
|
+
const formId = extractFormId(createResult);
|
|
11928
|
+
if (!formId) {
|
|
11929
|
+
throw new Error(`create_form returned no id: ${JSON.stringify(createResult).slice(0, 200)}`);
|
|
11930
|
+
}
|
|
11931
|
+
try {
|
|
11932
|
+
for (let attempt = 1; ; attempt++) {
|
|
11933
|
+
try {
|
|
11934
|
+
await formApiRequest(builderClient, "POST", `/${formId}?locationId=${locationId2}`, { name, formData });
|
|
11935
|
+
break;
|
|
11936
|
+
} catch (saveErr) {
|
|
11937
|
+
if (isFormNotYetPropagated(saveErr) && attempt < 6) {
|
|
11938
|
+
await sleep2(700 * attempt);
|
|
11939
|
+
continue;
|
|
11940
|
+
}
|
|
11941
|
+
throw saveErr;
|
|
11942
|
+
}
|
|
11943
|
+
}
|
|
11944
|
+
let persisted = 0;
|
|
11945
|
+
for (let attempt = 1; attempt <= 6; attempt++) {
|
|
11946
|
+
const verify = await formApiRequest(builderClient, "GET", `/${formId}?locationId=${locationId2}`);
|
|
11947
|
+
persisted = countFormFields(verify);
|
|
11948
|
+
if (persisted > 0) break;
|
|
11949
|
+
if (attempt < 6) await sleep2(700 * attempt);
|
|
11950
|
+
}
|
|
11951
|
+
if (persisted === 0) {
|
|
11952
|
+
throw new Error(`form "${name}" was created (${formId}) but no fields persisted after save (read-after-write); not binding`);
|
|
11953
|
+
}
|
|
11954
|
+
return formId;
|
|
11955
|
+
} catch (err) {
|
|
11956
|
+
try {
|
|
11957
|
+
await formApiRequest(builderClient, "DELETE", `/${formId}?locationId=${locationId2}`);
|
|
11958
|
+
} catch {
|
|
11959
|
+
}
|
|
11960
|
+
throw err;
|
|
11961
|
+
}
|
|
11585
11962
|
}
|
|
11586
11963
|
};
|
|
11587
11964
|
}
|
|
@@ -11640,7 +12017,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11640
12017
|
);
|
|
11641
12018
|
server2.tool(
|
|
11642
12019
|
"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).
|
|
12020
|
+
`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), calendars, AND forms: 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). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they're surfaced as a manual step, not auto-staffed to a guess. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). 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
12021
|
{
|
|
11645
12022
|
plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
11646
12023
|
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)."),
|
|
@@ -11715,6 +12092,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11715
12092
|
}
|
|
11716
12093
|
const deps = makeExecuteDeps(client, builderClient, activeLocation);
|
|
11717
12094
|
const exec = await executeBackbone(typedPlan, deps);
|
|
12095
|
+
const calendarManualLines = exec.manual.map((m) => `[calendar] ${m.reason}`);
|
|
11718
12096
|
const manualLines = result.workflows.flatMap((w) => [
|
|
11719
12097
|
...w.manual.map((m) => `[${w.name}] ${m.reason}`),
|
|
11720
12098
|
...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
|
|
@@ -11729,11 +12107,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11729
12107
|
scanWarnings,
|
|
11730
12108
|
halted: exec.halted,
|
|
11731
12109
|
built: exec.built,
|
|
12110
|
+
manual: exec.manual,
|
|
11732
12111
|
idMap: exec.idMap,
|
|
11733
12112
|
deferred: exec.deferred,
|
|
11734
|
-
deferredNote: "
|
|
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.`
|
|
12113
|
+
deferredNote: "execute builds the CRM backbone (pipelines, custom fields, tags, custom values), calendars, and forms live. Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole user; otherwise they're listed under manual steps. Funnels/workflows are planned but NOT auto-built yet \u2014 create them via the GHL UI or the dedicated tools, in this order: funnels \u2192 workflows.",
|
|
12114
|
+
nextManualSteps: [...calendarManualLines, ...manualLines, ...handoffLines],
|
|
12115
|
+
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.manual.length ? ` ${exec.manual.length} calendar(s) need manual staff assignment.` : ""}${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
12116
|
});
|
|
11738
12117
|
}
|
|
11739
12118
|
const collisions = result.items.filter((i) => i.status === "existing");
|
|
@@ -11768,9 +12147,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11768
12147
|
gatedBy: w.gatedBy,
|
|
11769
12148
|
draft: w.gatedBy.length > 0 || !(publishWorkflows ?? false)
|
|
11770
12149
|
})),
|
|
12150
|
+
calendarsManual: result.calendarsManual,
|
|
11771
12151
|
handoffs: result.handoffs,
|
|
11772
12152
|
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).
|
|
12153
|
+
next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone + calendars live (pipelines, fields, tags, custom values, calendars). Forms/funnels/workflows are listed as manual next steps.'
|
|
11774
12154
|
});
|
|
11775
12155
|
} catch (error) {
|
|
11776
12156
|
return errorResponse(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.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",
|