@elitedcs/ghl-mcp 3.36.0 → 3.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +994 -18
  2. 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.36.0",
34
+ version: "3.38.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 msg = err instanceof Error ? err.message : String(err);
6541
- return { ok: false, error: `Could not reach license server: ${msg}`, reason: "unreachable" };
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 msg = err instanceof Error ? err.message : String(err);
6580
- return { ok: false, error: `Could not reach GHL: ${msg}` };
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 msg = err instanceof Error ? err.message : String(err);
6597
- return { ok: false, error: `Could not reach Firebase: ${msg}` };
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 msg = error instanceof Error ? error.message : String(error);
7421
- results.errors.push(`${contactId}: ${msg}`);
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
- action: import_zod52.z.string().optional()
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,685 @@ 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 STAFF_CALENDAR_TYPES = /* @__PURE__ */ new Set([
10855
+ "round_robin",
10856
+ "collective",
10857
+ "class_booking",
10858
+ "service_booking"
10859
+ ]);
10860
+ function calendarNeedsStaff(cal) {
10861
+ return cal.requiresStaff === true || STAFF_CALENDAR_TYPES.has(cal.calendarType);
10862
+ }
10863
+ function classifyCalendarBuild(cal, userCount) {
10864
+ if (!calendarNeedsStaff(cal)) return { action: "build" };
10865
+ if (userCount === 1) return { action: "build" };
10866
+ if (userCount === void 0) {
10867
+ return {
10868
+ action: "manual",
10869
+ 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.`
10870
+ };
10871
+ }
10872
+ if (userCount === 0) {
10873
+ return {
10874
+ action: "manual",
10875
+ 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.`
10876
+ };
10877
+ }
10878
+ return {
10879
+ action: "manual",
10880
+ 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.`
10881
+ };
10882
+ }
10883
+ var norm = (s) => s.trim().toLowerCase();
10884
+ function buildRefIndex(plan) {
10885
+ const idx = {
10886
+ tagName: /* @__PURE__ */ new Map(),
10887
+ fieldName: /* @__PURE__ */ new Map(),
10888
+ fieldType: /* @__PURE__ */ new Map(),
10889
+ email: /* @__PURE__ */ new Map(),
10890
+ sms: /* @__PURE__ */ new Map(),
10891
+ workflowName: /* @__PURE__ */ new Map(),
10892
+ pipelineName: /* @__PURE__ */ new Map(),
10893
+ stageName: /* @__PURE__ */ new Map()
10894
+ };
10895
+ for (const t of plan.tags ?? []) idx.tagName.set(t.ref, t.name);
10896
+ for (const f of plan.customFields ?? []) {
10897
+ idx.fieldName.set(f.ref, f.name);
10898
+ idx.fieldType.set(f.ref, f.dataType);
10899
+ }
10900
+ for (const e of plan.emails ?? []) idx.email.set(e.ref, { subject: e.subject, body: e.body, bodyOutline: e.bodyOutline, name: e.name });
10901
+ for (const s of plan.sms ?? []) idx.sms.set(s.ref, { body: s.body, bodyOutline: s.bodyOutline, name: s.name });
10902
+ for (const w of plan.workflows ?? []) idx.workflowName.set(w.ref, w.name);
10903
+ for (const p of plan.pipelines ?? []) {
10904
+ idx.pipelineName.set(p.ref, p.name);
10905
+ for (const st of p.stages) idx.stageName.set(st.ref, st.name);
10906
+ }
10907
+ return idx;
10908
+ }
10909
+ function htmlWrap(text) {
10910
+ const t = text.trim();
10911
+ if (/^\s*<[a-z]/i.test(t)) return t;
10912
+ return `<p style="margin:0px;">${t}</p>`;
10913
+ }
10914
+ function resolveId(ref, idMap) {
10915
+ const real = idMap.get(ref);
10916
+ return real ?? PENDING(ref);
10917
+ }
10918
+ function expandAction(action, idx, idMap) {
10919
+ switch (action.type) {
10920
+ case "add_contact_tag":
10921
+ case "remove_contact_tag": {
10922
+ const name = idx.tagName.get(action.tagRef) ?? action.tagRef;
10923
+ return {
10924
+ kind: "expanded",
10925
+ pendingRefs: [],
10926
+ native: {
10927
+ type: action.type,
10928
+ name: action.type === "add_contact_tag" ? `Add tag: ${name}` : `Remove tag: ${name}`,
10929
+ attributes: { tags: [name] }
10930
+ }
10931
+ };
10932
+ }
10933
+ case "send_email": {
10934
+ const e = idx.email.get(action.emailRef);
10935
+ if (!e) return { kind: "manual", logicalType: action.type, reason: `email ref ${action.emailRef} not found in plan` };
10936
+ if (!e.body) {
10937
+ return {
10938
+ kind: "needs_content",
10939
+ logicalType: action.type,
10940
+ reason: `email "${action.emailRef}" has only an outline (no send-ready body) \u2014 supply copy before this email step can be built`
10941
+ };
10942
+ }
10943
+ return {
10944
+ kind: "expanded",
10945
+ pendingRefs: [],
10946
+ native: {
10947
+ type: "email",
10948
+ name: `Email: ${e.name}`,
10949
+ attributes: {
10950
+ subject: e.subject ?? e.name,
10951
+ html: htmlWrap(e.body),
10952
+ trackingOptions: { hasTrackingLinks: false, hasUtmTracking: false, hasTags: false }
10953
+ }
10954
+ }
10955
+ };
10956
+ }
10957
+ case "send_sms": {
10958
+ const s = idx.sms.get(action.smsRef);
10959
+ if (!s) return { kind: "manual", logicalType: action.type, reason: `sms ref ${action.smsRef} not found in plan` };
10960
+ if (!s.body) {
10961
+ return {
10962
+ kind: "needs_content",
10963
+ logicalType: action.type,
10964
+ reason: `sms "${action.smsRef}" has only an outline (no send-ready body) \u2014 supply copy before this SMS step can be built`
10965
+ };
10966
+ }
10967
+ return {
10968
+ kind: "expanded",
10969
+ pendingRefs: [],
10970
+ native: {
10971
+ type: "sms",
10972
+ name: `SMS: ${s.name}`,
10973
+ attributes: { body: s.body, attachments: [] }
10974
+ }
10975
+ };
10976
+ }
10977
+ case "wait": {
10978
+ return {
10979
+ kind: "expanded",
10980
+ pendingRefs: [],
10981
+ native: {
10982
+ type: "wait",
10983
+ name: "Wait",
10984
+ attributes: {
10985
+ type: "time",
10986
+ startAfter: { type: WAIT_UNIT_MAP[action.unit], value: action.value, when: "after" },
10987
+ name: "Wait",
10988
+ isHybridAction: true,
10989
+ hybridActionType: "wait",
10990
+ convertToMultipath: false,
10991
+ transitions: []
10992
+ }
10993
+ }
10994
+ };
10995
+ }
10996
+ case "internal_notification": {
10997
+ const looksLikeUserId = /^[A-Za-z0-9]{17,}$/.test(action.to);
10998
+ return {
10999
+ kind: "expanded",
11000
+ pendingRefs: [],
11001
+ native: {
11002
+ type: "internal_notification",
11003
+ name: `Notify: ${action.title}`,
11004
+ attributes: {
11005
+ type: "notification",
11006
+ notification: {
11007
+ body: action.body,
11008
+ title: action.title,
11009
+ userType: "user",
11010
+ redirectPage: "contact",
11011
+ type: "send_notification",
11012
+ selectedUser: looksLikeUserId ? action.to : ""
11013
+ }
11014
+ }
11015
+ }
11016
+ };
11017
+ }
11018
+ case "update_contact_field": {
11019
+ const fieldId = resolveId(action.fieldRef, idMap);
11020
+ const title = idx.fieldName.get(action.fieldRef) ?? action.fieldRef;
11021
+ return {
11022
+ kind: "expanded",
11023
+ pendingRefs: isPending(fieldId) ? [action.fieldRef] : [],
11024
+ native: {
11025
+ type: "update_contact_field",
11026
+ name: `Update field: ${title}`,
11027
+ attributes: {
11028
+ type: "update_contact_field",
11029
+ actionType: "update_field_data",
11030
+ fields: [{ field: fieldId, value: action.value, title, type: "text", date: "" }]
11031
+ }
11032
+ }
11033
+ };
11034
+ }
11035
+ case "add_notes": {
11036
+ return {
11037
+ kind: "expanded",
11038
+ pendingRefs: [],
11039
+ native: { type: "add_notes", name: "Add note", attributes: { type: "add_notes", html: htmlWrap(action.body) } }
11040
+ };
11041
+ }
11042
+ case "task_notification": {
11043
+ return {
11044
+ kind: "expanded",
11045
+ pendingRefs: [],
11046
+ native: {
11047
+ type: "task-notification",
11048
+ name: `Task: ${action.title}`,
11049
+ attributes: {
11050
+ assignedTo: action.assignedTo ?? "",
11051
+ title: action.title,
11052
+ dueDate: action.dueDate ?? "1",
11053
+ body: action.body ?? "",
11054
+ type: "task-notification",
11055
+ __customInputs__: {}
11056
+ }
11057
+ }
11058
+ };
11059
+ }
11060
+ case "remove_from_workflow":
11061
+ case "add_to_workflow": {
11062
+ const wfId = resolveId(action.workflowRef, idMap);
11063
+ const wfName = idx.workflowName.get(action.workflowRef) ?? action.workflowRef;
11064
+ const pending = isPending(wfId) ? [action.workflowRef] : [];
11065
+ if (action.type === "remove_from_workflow") {
11066
+ return {
11067
+ kind: "expanded",
11068
+ pendingRefs: pending,
11069
+ native: {
11070
+ type: "remove_from_workflow",
11071
+ name: `Remove from: ${wfName}`,
11072
+ attributes: { workflowId: wfId, workflowName: wfName, type: "remove_from_workflow", workflow_id: [wfId] }
11073
+ }
11074
+ };
11075
+ }
11076
+ return {
11077
+ kind: "expanded",
11078
+ pendingRefs: pending,
11079
+ native: {
11080
+ type: "add_to_workflow",
11081
+ name: `Add to: ${wfName}`,
11082
+ attributes: { workflowId: wfId, workflowName: wfName, type: "add_to_workflow", workflow_id: [wfId] }
11083
+ }
11084
+ };
11085
+ }
11086
+ case "create_opportunity":
11087
+ case "update_opportunity": {
11088
+ const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
11089
+ const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
11090
+ const verb = action.type === "create_opportunity" ? "Create" : "Move";
11091
+ return {
11092
+ kind: "manual",
11093
+ logicalType: action.type,
11094
+ 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.`
11095
+ };
11096
+ }
11097
+ case "goal_event": {
11098
+ return {
11099
+ kind: "expanded",
11100
+ pendingRefs: [],
11101
+ native: {
11102
+ type: "workflow_goal",
11103
+ name: "Goal",
11104
+ attributes: {
11105
+ op: "or",
11106
+ segments: [{ op: "or", conditions: [{ goal_condition: action.goalCondition, id: "" }] }],
11107
+ type: "workflow_goal",
11108
+ action: action.action ?? "exit"
11109
+ }
11110
+ }
11111
+ };
11112
+ }
11113
+ default: {
11114
+ const _exhaustive = action;
11115
+ return { kind: "manual", logicalType: _exhaustive.type, reason: "unrecognized logical action type" };
11116
+ }
11117
+ }
11118
+ }
11119
+ var MAX_ACTIONS_PER_WORKFLOW = 40;
11120
+ function expandWorkflow(workflow, idx, idMap, gatedBy) {
11121
+ const nativeActions = [];
11122
+ const manual = [];
11123
+ const needsContent = [];
11124
+ const pendingRefs = /* @__PURE__ */ new Set();
11125
+ workflow.actions.forEach((a, i) => {
11126
+ const exp = expandAction(a, idx, idMap);
11127
+ if (exp.kind === "expanded") {
11128
+ nativeActions.push(exp.native);
11129
+ exp.pendingRefs.forEach((r) => pendingRefs.add(r));
11130
+ } else if (exp.kind === "manual") {
11131
+ manual.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
11132
+ } else {
11133
+ needsContent.push({ index: i, logicalType: exp.logicalType, reason: exp.reason });
11134
+ }
11135
+ });
11136
+ const splitInto = Math.max(1, Math.ceil(nativeActions.length / MAX_ACTIONS_PER_WORKFLOW));
11137
+ return {
11138
+ ref: workflow.ref,
11139
+ name: workflow.name,
11140
+ nativeActions,
11141
+ manual,
11142
+ needsContent,
11143
+ pendingRefs: [...pendingRefs],
11144
+ splitInto,
11145
+ gatedBy
11146
+ };
11147
+ }
11148
+ function scanSection(section2, planObjects, existing) {
11149
+ const byName = /* @__PURE__ */ new Map();
11150
+ for (const e of existing ?? []) byName.set(norm(e.name), e);
11151
+ return planObjects.map((o) => {
11152
+ const hit = byName.get(norm(o.name));
11153
+ 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" };
11154
+ });
11155
+ }
11156
+ function computeWorkflowGates(plan, metHandoffs) {
11157
+ const gates = /* @__PURE__ */ new Map();
11158
+ const addGate = (wfRef, handoffRef) => {
11159
+ const cur = gates.get(wfRef) ?? [];
11160
+ if (!cur.includes(handoffRef)) cur.push(handoffRef);
11161
+ gates.set(wfRef, cur);
11162
+ };
11163
+ const workflowRefs = (plan.workflows ?? []).map((w) => w.ref);
11164
+ for (const h of plan.handoffs ?? []) {
11165
+ if (metHandoffs.has(h.ref)) continue;
11166
+ for (const block of h.blocks ?? []) {
11167
+ if (block.endsWith(".*")) {
11168
+ if (block === "workflow.*") for (const wf of workflowRefs) addGate(wf, h.ref);
11169
+ } else if (refNamespace(block) === "workflow") {
11170
+ addGate(block, h.ref);
11171
+ }
11172
+ }
11173
+ }
11174
+ return gates;
11175
+ }
11176
+ var SECTION_OBJECTS = {
11177
+ pipelines: (p) => (p.pipelines ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11178
+ customFields: (p) => (p.customFields ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11179
+ tags: (p) => (p.tags ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11180
+ customValues: (p) => (p.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11181
+ calendars: (p) => (p.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11182
+ forms: (p) => (p.forms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11183
+ funnels: (p) => (p.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11184
+ emails: (p) => (p.emails ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11185
+ sms: (p) => (p.sms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11186
+ workflows: (p) => (p.workflows ?? []).map((x) => ({ ref: x.ref, name: x.name }))
11187
+ };
11188
+ var SECTION_EXISTING = {
11189
+ pipelines: "pipelines",
11190
+ customFields: "customFields",
11191
+ tags: "tags",
11192
+ customValues: "customValues",
11193
+ calendars: "calendars",
11194
+ forms: "forms",
11195
+ funnels: "funnels",
11196
+ emails: null,
11197
+ // emails/sms live inside workflows; no standalone clobber scan
11198
+ sms: null,
11199
+ workflows: "workflows"
11200
+ };
11201
+ function resolvePlan(plan, existing, opts = {}) {
11202
+ const idx = buildRefIndex(plan);
11203
+ const metHandoffs = new Set(opts.metHandoffs ?? []);
11204
+ const seededIdMap = new Map(Object.entries(opts.idMap ?? {}));
11205
+ const items = [];
11206
+ const idMap = new Map(seededIdMap);
11207
+ for (const section2 of EXECUTION_ORDER) {
11208
+ const objs = SECTION_OBJECTS[section2](plan);
11209
+ if (objs.length === 0) continue;
11210
+ const existingKey = SECTION_EXISTING[section2];
11211
+ const existingList = existingKey ? existing[existingKey] : void 0;
11212
+ const scanned = scanSection(section2, objs, existingList);
11213
+ for (const it of scanned) {
11214
+ items.push(it);
11215
+ if (it.status === "existing" && it.existingId) idMap.set(it.ref, it.existingId);
11216
+ else if (!idMap.has(it.ref)) idMap.set(it.ref, PENDING(it.ref));
11217
+ }
11218
+ if (section2 === "pipelines") {
11219
+ for (const p of plan.pipelines ?? []) {
11220
+ for (const st of p.stages) if (!idMap.has(st.ref)) idMap.set(st.ref, PENDING(st.ref));
11221
+ }
11222
+ }
11223
+ }
11224
+ const existingCalNames = new Set((existing.calendars ?? []).map((c) => norm(c.name)));
11225
+ const calendarsManual = [];
11226
+ for (const cal of plan.calendars ?? []) {
11227
+ if (existingCalNames.has(norm(cal.name))) continue;
11228
+ const decision = classifyCalendarBuild(cal, existing.userCount);
11229
+ if (decision.action === "manual") {
11230
+ calendarsManual.push({ ref: cal.ref, name: cal.name, reason: decision.reason ?? "needs a team member" });
11231
+ }
11232
+ }
11233
+ const gates = computeWorkflowGates(plan, metHandoffs);
11234
+ const workflows = (plan.workflows ?? []).map((w) => expandWorkflow(w, idx, idMap, gates.get(w.ref) ?? []));
11235
+ const handoffs = (plan.handoffs ?? []).map((h) => ({
11236
+ ref: h.ref,
11237
+ owner: h.owner,
11238
+ title: h.title,
11239
+ instruction: h.instruction,
11240
+ successCheck: h.successCheck,
11241
+ met: metHandoffs.has(h.ref),
11242
+ blocks: h.blocks ?? []
11243
+ }));
11244
+ const manualCalRefs = new Set(calendarsManual.map((c) => c.ref));
11245
+ const summary = {
11246
+ wouldCreate: items.filter((i) => i.status === "would_create" && !manualCalRefs.has(i.ref)).length,
11247
+ existing: items.filter((i) => i.status === "existing").length,
11248
+ workflowsTotal: workflows.length,
11249
+ workflowsGated: workflows.filter((w) => w.gatedBy.length > 0).length,
11250
+ actionsExpanded: workflows.reduce((n, w) => n + w.nativeActions.length, 0),
11251
+ actionsManual: workflows.reduce((n, w) => n + w.manual.length, 0),
11252
+ actionsNeedContent: workflows.reduce((n, w) => n + w.needsContent.length, 0)
11253
+ };
11254
+ return { items, idMap: Object.fromEntries(idMap), workflows, calendarsManual, handoffs, summary };
11255
+ }
11256
+ function renderReport(plan, result, ctx) {
11257
+ const L = [];
11258
+ const { summary } = result;
11259
+ L.push(`Blueprint build ${ctx.mode === "dry_run" ? "PREVIEW (dry run \u2014 no changes written)" : "REPORT"}`);
11260
+ L.push(`Account: ${ctx.locationName} (${ctx.locationId})`);
11261
+ L.push(`Plan: ${plan.planId} \xB7 preset: ${plan.preset}`);
11262
+ L.push("");
11263
+ L.push(
11264
+ `Objects: ${summary.wouldCreate} to create, ${summary.existing} already exist (skipped, never modified).`
11265
+ );
11266
+ L.push(
11267
+ `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.`
11268
+ );
11269
+ L.push("");
11270
+ const manualCalRefs = new Set(result.calendarsManual.map((c) => c.ref));
11271
+ L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
11272
+ for (const section2 of EXECUTION_ORDER) {
11273
+ const secItems = result.items.filter((i) => i.type === section2);
11274
+ if (secItems.length === 0) continue;
11275
+ for (const it of secItems) {
11276
+ if (manualCalRefs.has(it.ref)) continue;
11277
+ const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
11278
+ L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
11279
+ }
11280
+ }
11281
+ for (const w of result.workflows) {
11282
+ const gate = w.gatedBy.length ? ` \u2014 DRAFT, gated by ${w.gatedBy.join(", ")}` : ctx.publishWorkflows ? " \u2014 publish" : " \u2014 DRAFT";
11283
+ const split = w.splitInto > 1 ? ` (splits into ${w.splitInto} chained workflows, >${MAX_ACTIONS_PER_WORKFLOW} actions)` : "";
11284
+ L.push(` [workflow] ${w.name}: ${w.nativeActions.length} actions${split}${gate}`);
11285
+ }
11286
+ L.push("");
11287
+ L.push("\u2500\u2500 You must do these by hand (in order) \u2500\u2500");
11288
+ let any = false;
11289
+ for (const c of result.calendarsManual) {
11290
+ any = true;
11291
+ L.push(` \u2022 [calendar] ${c.reason}`);
11292
+ }
11293
+ for (const w of result.workflows) {
11294
+ for (const m of w.manual) {
11295
+ any = true;
11296
+ L.push(` \u2022 [${w.name}] ${m.reason}`);
11297
+ }
11298
+ for (const nc of w.needsContent) {
11299
+ any = true;
11300
+ L.push(` \u2022 [${w.name}] ${nc.reason}`);
11301
+ }
11302
+ }
11303
+ for (const h of result.handoffs) {
11304
+ if (h.met) continue;
11305
+ any = true;
11306
+ L.push(` \u2022 [${h.owner}] ${h.title}: ${h.instruction} (done when: ${h.successCheck})`);
11307
+ }
11308
+ if (!any) L.push(" (nothing \u2014 everything in this plan is auto-buildable)");
11309
+ return L.join("\n");
11310
+ }
11311
+
11312
+ // src/intake-to-build/execute.ts
11313
+ var norm2 = (s) => s.trim().toLowerCase();
11314
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11315
+ async function executeBackbone(plan, deps, opts = {}) {
11316
+ const retries = opts.verifyRetries ?? 4;
11317
+ const backoff = opts.verifyBackoffMs ?? 500;
11318
+ const idMap = {};
11319
+ const built = [];
11320
+ const manual = [];
11321
+ const halt = (atRef, type, reason) => ({
11322
+ ok: false,
11323
+ idMap,
11324
+ built,
11325
+ manual,
11326
+ halted: { atRef, type, reason },
11327
+ deferred: deferredSections(plan)
11328
+ });
11329
+ async function pollForNew(read, name, beforeIds) {
11330
+ for (let attempt = 1; attempt <= retries; attempt++) {
11331
+ const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
11332
+ if (fresh.length === 1) return fresh[0];
11333
+ if (fresh.length > 1) return void 0;
11334
+ if (attempt < retries) await sleep(backoff * attempt);
11335
+ }
11336
+ return void 0;
11337
+ }
11338
+ for (const p of plan.pipelines ?? []) {
11339
+ let pipelines;
11340
+ try {
11341
+ pipelines = await deps.listPipelines();
11342
+ } catch (e) {
11343
+ return halt(p.ref, "pipeline", `could not read existing pipelines: ${msg(e)}`);
11344
+ }
11345
+ const matches = pipelines.filter((x) => norm2(x.name) === norm2(p.name));
11346
+ if (matches.length > 1) {
11347
+ 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.`);
11348
+ }
11349
+ let pipeline;
11350
+ let created;
11351
+ if (matches.length === 1) {
11352
+ pipeline = matches[0];
11353
+ created = false;
11354
+ } else {
11355
+ const beforeIds = new Set(pipelines.map((x) => x.id));
11356
+ try {
11357
+ await deps.createPipeline(p.name, p.stages.map((s) => ({ name: s.name, position: s.position })));
11358
+ } catch (e) {
11359
+ return halt(p.ref, "pipeline", `create failed: ${msg(e)}`);
11360
+ }
11361
+ const verified = await pollForNew(() => deps.listPipelines(), p.name, beforeIds);
11362
+ 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)");
11363
+ pipeline = verified;
11364
+ created = true;
11365
+ }
11366
+ idMap[p.ref] = pipeline.id;
11367
+ built.push({ ref: p.ref, type: "pipeline", name: p.name, status: created ? "created" : "existing", realId: pipeline.id });
11368
+ for (const st of p.stages) {
11369
+ const stageMatches = pipeline.stages.filter((es) => norm2(es.name) === norm2(st.name));
11370
+ if (stageMatches.length === 0) {
11371
+ return halt(st.ref, "stage", `pipeline "${p.name}" has no stage named "${st.name}" after build \u2014 cannot resolve ${st.ref}`);
11372
+ }
11373
+ if (stageMatches.length > 1) {
11374
+ return halt(st.ref, "stage", `pipeline "${p.name}" has ${stageMatches.length} stages named "${st.name}" \u2014 cannot resolve ${st.ref} to a single id`);
11375
+ }
11376
+ idMap[st.ref] = stageMatches[0].id;
11377
+ built.push({ ref: st.ref, type: "stage", name: st.name, status: created ? "created" : "existing", realId: stageMatches[0].id });
11378
+ }
11379
+ }
11380
+ const fieldHalt = await buildSimple(
11381
+ plan.customFields ?? [],
11382
+ "field",
11383
+ () => deps.listCustomFields(),
11384
+ (f) => deps.createCustomField({ name: f.name, dataType: f.dataType, model: f.model, options: f.options }),
11385
+ (f) => f.name,
11386
+ pollForNew,
11387
+ idMap,
11388
+ built
11389
+ );
11390
+ if (fieldHalt) return halt(fieldHalt.ref, "field", fieldHalt.reason);
11391
+ const tagHalt = await buildSimple(
11392
+ plan.tags ?? [],
11393
+ "tag",
11394
+ () => deps.listTags(),
11395
+ (t) => deps.createTag(t.name),
11396
+ (t) => t.name,
11397
+ pollForNew,
11398
+ idMap,
11399
+ built
11400
+ );
11401
+ if (tagHalt) return halt(tagHalt.ref, "tag", tagHalt.reason);
11402
+ const cvHalt = await buildSimple(
11403
+ plan.customValues ?? [],
11404
+ "cv",
11405
+ () => deps.listCustomValues(),
11406
+ (cv) => deps.createCustomValue(cv.name, cv.value ?? ""),
11407
+ (cv) => cv.name,
11408
+ pollForNew,
11409
+ idMap,
11410
+ built
11411
+ );
11412
+ if (cvHalt) return halt(cvHalt.ref, "cv", cvHalt.reason);
11413
+ let cachedUserCount;
11414
+ let cachedSoloUserId;
11415
+ let usersRead = false;
11416
+ for (const cal of plan.calendars ?? []) {
11417
+ let calendars;
11418
+ try {
11419
+ calendars = await deps.listCalendars();
11420
+ } catch (e) {
11421
+ return halt(cal.ref, "calendar", `could not read existing calendars: ${msg(e)}`);
11422
+ }
11423
+ const matches = calendars.filter((c) => norm2(c.name) === norm2(cal.name));
11424
+ if (matches.length > 1) {
11425
+ 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.`);
11426
+ }
11427
+ if (matches.length === 1) {
11428
+ idMap[cal.ref] = matches[0].id;
11429
+ built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "existing", realId: matches[0].id });
11430
+ continue;
11431
+ }
11432
+ let staffUserId;
11433
+ if (calendarNeedsStaff(cal)) {
11434
+ if (!usersRead) {
11435
+ try {
11436
+ const users = await deps.listUsers();
11437
+ cachedUserCount = users.length;
11438
+ cachedSoloUserId = users.length === 1 ? users[0].id : void 0;
11439
+ } catch {
11440
+ cachedUserCount = void 0;
11441
+ }
11442
+ usersRead = true;
11443
+ }
11444
+ const decision = classifyCalendarBuild(cal, cachedUserCount);
11445
+ if (decision.action === "manual") {
11446
+ manual.push({ ref: cal.ref, type: "calendar", name: cal.name, reason: decision.reason ?? "needs a team member" });
11447
+ continue;
11448
+ }
11449
+ staffUserId = cachedSoloUserId;
11450
+ }
11451
+ const beforeIds = new Set(calendars.map((c) => c.id));
11452
+ try {
11453
+ await deps.createCalendar({
11454
+ name: cal.name,
11455
+ calendarType: cal.calendarType,
11456
+ openHours: cal.openHours,
11457
+ availabilityType: cal.availabilityType,
11458
+ staffUserId
11459
+ });
11460
+ } catch (e) {
11461
+ return halt(cal.ref, "calendar", `create failed: ${msg(e)}`);
11462
+ }
11463
+ const verified = await pollForNew(() => deps.listCalendars(), cal.name, beforeIds);
11464
+ 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)");
11465
+ idMap[cal.ref] = verified.id;
11466
+ built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "created", realId: verified.id });
11467
+ }
11468
+ return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
11469
+ }
11470
+ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
11471
+ if (objects.length === 0) return null;
11472
+ for (const obj of objects) {
11473
+ const name = nameOf(obj);
11474
+ let existing;
11475
+ try {
11476
+ existing = await list();
11477
+ } catch (e) {
11478
+ return { ref: obj.ref, reason: `could not read existing ${type}s: ${msg(e)}` };
11479
+ }
11480
+ const matches = existing.filter((o) => norm2(o.name) === norm2(name));
11481
+ if (matches.length > 1) {
11482
+ 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.` };
11483
+ }
11484
+ if (matches.length === 1) {
11485
+ idMap[obj.ref] = matches[0].id;
11486
+ built.push({ ref: obj.ref, type, name, status: "existing", realId: matches[0].id });
11487
+ continue;
11488
+ }
11489
+ const beforeIds = new Set(existing.map((o) => o.id));
11490
+ try {
11491
+ await create(obj);
11492
+ } catch (e) {
11493
+ return { ref: obj.ref, reason: `create failed: ${msg(e)}` };
11494
+ }
11495
+ const verified = await pollForNew(list, name, beforeIds);
11496
+ 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)` };
11497
+ idMap[obj.ref] = verified.id;
11498
+ built.push({ ref: obj.ref, type, name, status: "created", realId: verified.id });
11499
+ }
11500
+ return null;
11501
+ }
11502
+ function deferredSections(plan) {
11503
+ const out = [];
11504
+ if (plan.forms?.length) out.push({ section: "forms", count: plan.forms.length });
11505
+ if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
11506
+ if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
11507
+ return out;
11508
+ }
11509
+ function msg(e) {
11510
+ return e instanceof Error ? e.message : String(e);
11511
+ }
11512
+
10821
11513
  // src/tools/intake-to-build.ts
10822
11514
  var customFieldItemSchema = import_zod53.z.object({
10823
11515
  id: import_zod53.z.string(),
@@ -10875,10 +11567,152 @@ function findRecordForQuestion(q, records) {
10875
11567
  const wantName = intakeFieldName(q.label).toLowerCase();
10876
11568
  return records.find((r) => r.name.toLowerCase() === wantName);
10877
11569
  }
10878
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11570
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
10879
11571
  function isFormNotYetPropagated(error) {
10880
- const msg = error instanceof Error ? error.message : String(error);
10881
- return /does not exist or is deleted/i.test(msg);
11572
+ const msg2 = error instanceof Error ? error.message : String(error);
11573
+ return /does not exist or is deleted/i.test(msg2);
11574
+ }
11575
+ function pickObjects(raw, keys) {
11576
+ const root = raw && typeof raw === "object" ? raw : {};
11577
+ let list = Array.isArray(raw) ? raw : void 0;
11578
+ if (!list) {
11579
+ for (const k of keys) {
11580
+ if (Array.isArray(root[k])) {
11581
+ list = root[k];
11582
+ break;
11583
+ }
11584
+ }
11585
+ }
11586
+ if (!Array.isArray(list)) return [];
11587
+ const out = [];
11588
+ for (const item of list) {
11589
+ if (!item || typeof item !== "object") continue;
11590
+ const o = item;
11591
+ const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : void 0;
11592
+ const name = typeof o.name === "string" ? o.name : void 0;
11593
+ if (id && name) out.push({ id, name });
11594
+ }
11595
+ return out;
11596
+ }
11597
+ async function scanExistingAssets(client, locationId2) {
11598
+ const warnings = [];
11599
+ const assets = {};
11600
+ const read = async (label, fn, into) => {
11601
+ try {
11602
+ assets[into] = await fn();
11603
+ } catch (e) {
11604
+ warnings.push(`could not scan ${label}: ${e instanceof Error ? e.message : String(e)}`);
11605
+ }
11606
+ };
11607
+ await read("pipelines", async () => pickObjects(await client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }), ["pipelines"]), "pipelines");
11608
+ await read("custom fields", async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]), "customFields");
11609
+ await read("tags", async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]), "tags");
11610
+ await read("custom values", async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]), "customValues");
11611
+ await read("calendars", async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]), "calendars");
11612
+ await read("forms", async () => pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } }), ["forms"]), "forms");
11613
+ await read("funnels", async () => pickObjects(await client.get("/funnels/funnel/list", { params: { locationId: locationId2, limit: 100 } }), ["funnels"]), "funnels");
11614
+ await read("workflows", async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]), "workflows");
11615
+ try {
11616
+ assets.userCount = pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]).length;
11617
+ } catch (e) {
11618
+ warnings.push(`could not scan users (staff-requiring calendars will defer to a manual step): ${e instanceof Error ? e.message : String(e)}`);
11619
+ }
11620
+ return { assets, warnings };
11621
+ }
11622
+ function pickPipelines(raw) {
11623
+ const root = raw && typeof raw === "object" ? raw : {};
11624
+ const list = Array.isArray(root.pipelines) ? root.pipelines : Array.isArray(raw) ? raw : [];
11625
+ const out = [];
11626
+ for (const item of list) {
11627
+ if (!item || typeof item !== "object") continue;
11628
+ const p = item;
11629
+ const id = typeof p.id === "string" ? p.id : typeof p._id === "string" ? p._id : void 0;
11630
+ const name = typeof p.name === "string" ? p.name : void 0;
11631
+ if (!id || !name) continue;
11632
+ const stagesRaw = Array.isArray(p.stages) ? p.stages : [];
11633
+ const stages = stagesRaw.filter((s) => !!s && typeof s === "object").map((s) => ({
11634
+ id: typeof s.id === "string" ? s.id : typeof s._id === "string" ? s._id : "",
11635
+ name: typeof s.name === "string" ? s.name : "",
11636
+ position: typeof s.position === "number" ? s.position : void 0
11637
+ })).filter((s) => s.id && s.name);
11638
+ out.push({ id, name, stages });
11639
+ }
11640
+ return out;
11641
+ }
11642
+ function makeExecuteDeps(client, builderClient, locationId2) {
11643
+ const pipelineApi = async (method, path7, body) => {
11644
+ const headers = await builderClient.buildHeaders();
11645
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path7}`;
11646
+ const options = { method, headers };
11647
+ if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
11648
+ const response = await fetch(url, options);
11649
+ if (!response.ok) {
11650
+ const text2 = await response.text();
11651
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path7}
11652
+ ${text2.slice(0, 300)}`);
11653
+ }
11654
+ const text = await response.text();
11655
+ if (!text) return {};
11656
+ try {
11657
+ return JSON.parse(text);
11658
+ } catch {
11659
+ return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
11660
+ }
11661
+ };
11662
+ return {
11663
+ listPipelines: async () => pickPipelines(await pipelineApi("GET", `?locationId=${locationId2}`)),
11664
+ createPipeline: async (name, stages) => {
11665
+ await pipelineApi("POST", "", {
11666
+ name,
11667
+ stages: stages.map((s) => ({ name: s.name, position: s.position, showInFunnel: true, showInPieChart: true })),
11668
+ locationId: locationId2,
11669
+ showInFunnel: true,
11670
+ showInPieChart: true
11671
+ });
11672
+ },
11673
+ // noRetry on every CREATE: these POSTs are not idempotent, so an auto-retry
11674
+ // after a lost response (429/5xx/network) would DUPLICATE the object in the
11675
+ // live account. With noRetry, a failed create simply fails → the executor's
11676
+ // verify-after catches it and halts; the idempotent re-run then binds the
11677
+ // one real object instead of stacking a second.
11678
+ listCustomFields: async () => pickObjects(await client.get(`/locations/${locationId2}/customFields`), ["customFields"]),
11679
+ createCustomField: async (f) => {
11680
+ const body = { name: f.name, dataType: f.dataType, model: f.model ?? "contact" };
11681
+ if (f.options && f.options.length) body.options = f.options;
11682
+ await client.post(`/locations/${locationId2}/customFields`, { body, noRetry: true });
11683
+ },
11684
+ listTags: async () => pickObjects(await client.get(`/locations/${locationId2}/tags`), ["tags"]),
11685
+ createTag: async (name) => {
11686
+ await client.post(`/locations/${locationId2}/tags`, { body: { name }, noRetry: true });
11687
+ },
11688
+ listCustomValues: async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]),
11689
+ createCustomValue: async (name, value) => {
11690
+ await client.post(`/locations/${locationId2}/customValues`, { body: { name, value }, noRetry: true });
11691
+ },
11692
+ listCalendars: async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]),
11693
+ // Users: count + the lone id (when solo) drive auto-staff. The /users/
11694
+ // endpoint 422s on a `limit` param under PIT auth, so pass only locationId.
11695
+ listUsers: async () => pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]),
11696
+ createCalendar: async (cal) => {
11697
+ const teamMembers = cal.staffUserId ? [{ userId: cal.staffUserId, priority: 1, isPrimary: true, selected: true, locationConfigurations: [{ kind: "custom", position: 0 }] }] : void 0;
11698
+ const body = buildCreateCalendarBody(
11699
+ { name: cal.name, calendarType: cal.calendarType, openHours: cal.openHours, availabilityType: cal.availabilityType, teamMembers },
11700
+ locationId2
11701
+ );
11702
+ await client.post("/calendars/", { body, noRetry: true });
11703
+ }
11704
+ };
11705
+ }
11706
+ async function readLocationName(client, locationId2) {
11707
+ try {
11708
+ const raw = await client.get(`/locations/${locationId2}`);
11709
+ const r = raw && typeof raw === "object" ? raw : {};
11710
+ const loc = r.location && typeof r.location === "object" ? r.location : r;
11711
+ const name = loc.name;
11712
+ return typeof name === "string" && name ? name : locationId2;
11713
+ } catch {
11714
+ return locationId2;
11715
+ }
10882
11716
  }
10883
11717
  function registerIntakeToBuildTools(server2, client, builderClient) {
10884
11718
  safeTool(
@@ -10922,6 +11756,148 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
10922
11756
  },
10923
11757
  async ({ plan }) => validateBuildPlan(plan)
10924
11758
  );
11759
+ server2.tool(
11760
+ "apply_build_plan",
11761
+ `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) AND calendars: 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/funnels/workflows are surfaced as manual next steps, not auto-built yet. Always confirms the active location and validates the plan before any write.`,
11762
+ {
11763
+ plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
11764
+ 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)."),
11765
+ locationId: import_zod53.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
11766
+ 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.'),
11767
+ publishWorkflows: import_zod53.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
11768
+ onConflict: import_zod53.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
11769
+ },
11770
+ async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
11771
+ try {
11772
+ const resolvedMode = mode ?? "dry_run";
11773
+ const activeLocation = client.resolveLocationId();
11774
+ if (locationId2 && locationId2 !== activeLocation) {
11775
+ return jsonResponse({
11776
+ ok: false,
11777
+ 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.)`
11778
+ });
11779
+ }
11780
+ const validation = validateBuildPlan(plan);
11781
+ if (!validation.valid || !validation.plan) {
11782
+ return jsonResponse({
11783
+ ok: false,
11784
+ phase: "validate",
11785
+ error: "Build plan failed \xA75 validation \u2014 fix these before building.",
11786
+ errors: validation.errors,
11787
+ warnings: validation.warnings
11788
+ });
11789
+ }
11790
+ const typedPlan = validation.plan;
11791
+ const locationName = await readLocationName(client, activeLocation);
11792
+ const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation);
11793
+ const result = resolvePlan(typedPlan, assets, { metHandoffs });
11794
+ if (resolvedMode === "execute") {
11795
+ if (!builderClient) {
11796
+ return jsonResponse({
11797
+ ok: false,
11798
+ phase: "execute",
11799
+ 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."
11800
+ });
11801
+ }
11802
+ if (builderClient.locationId && builderClient.locationId !== activeLocation) {
11803
+ return jsonResponse({
11804
+ ok: false,
11805
+ phase: "execute",
11806
+ 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.`
11807
+ });
11808
+ }
11809
+ try {
11810
+ await builderClient.buildHeaders();
11811
+ } catch (e) {
11812
+ 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.` });
11813
+ }
11814
+ const tokenCompany = builderClient.getTokenCompanyId();
11815
+ const intendedCompany = builderClient.getIntendedCompanyId();
11816
+ if (tokenCompany && intendedCompany && tokenCompany !== intendedCompany) {
11817
+ return jsonResponse({
11818
+ ok: false,
11819
+ phase: "execute",
11820
+ 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.`
11821
+ });
11822
+ }
11823
+ if (onConflict === "abort") {
11824
+ const collisions2 = result.items.filter((i) => i.status === "existing");
11825
+ if (collisions2.length > 0) {
11826
+ return jsonResponse({
11827
+ ok: false,
11828
+ phase: "execute",
11829
+ 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.`,
11830
+ collisions: collisions2.map((c) => ({ ref: c.ref, name: c.name, existingId: c.existingId }))
11831
+ });
11832
+ }
11833
+ }
11834
+ const deps = makeExecuteDeps(client, builderClient, activeLocation);
11835
+ const exec = await executeBackbone(typedPlan, deps);
11836
+ const calendarManualLines = exec.manual.map((m) => `[calendar] ${m.reason}`);
11837
+ const manualLines = result.workflows.flatMap((w) => [
11838
+ ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
11839
+ ...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
11840
+ ]);
11841
+ const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
11842
+ return jsonResponse({
11843
+ ok: exec.ok,
11844
+ mode: "execute",
11845
+ locationId: activeLocation,
11846
+ locationName,
11847
+ planId: typedPlan.planId,
11848
+ scanWarnings,
11849
+ halted: exec.halted,
11850
+ built: exec.built,
11851
+ manual: exec.manual,
11852
+ idMap: exec.idMap,
11853
+ deferred: exec.deferred,
11854
+ deferredNote: "execute builds the CRM backbone (pipelines, custom fields, tags, custom values) and calendars live. Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole user; otherwise they're listed under manual steps. Forms/funnels/workflows are planned but NOT auto-built yet \u2014 create them via the GHL UI or the dedicated tools, in this order: forms \u2192 funnels \u2192 workflows.",
11855
+ nextManualSteps: [...calendarManualLines, ...manualLines, ...handoffLines],
11856
+ 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.`
11857
+ });
11858
+ }
11859
+ const collisions = result.items.filter((i) => i.status === "existing");
11860
+ const aborted = onConflict === "abort" && collisions.length > 0;
11861
+ const report = renderReport(typedPlan, result, {
11862
+ mode: "dry_run",
11863
+ locationName,
11864
+ locationId: activeLocation,
11865
+ publishWorkflows: publishWorkflows ?? false
11866
+ });
11867
+ return jsonResponse({
11868
+ ok: true,
11869
+ mode: "dry_run",
11870
+ locationId: activeLocation,
11871
+ locationName,
11872
+ planId: typedPlan.planId,
11873
+ validation: { valid: true, warnings: validation.warnings, referencesScanned: validation.referencesScanned },
11874
+ scanWarnings,
11875
+ onConflict: onConflict ?? "skip",
11876
+ aborted: aborted ? { reason: `${collisions.length} same-named object(s) already exist and onConflict=abort`, collisions: collisions.map((c) => c.ref) } : void 0,
11877
+ summary: result.summary,
11878
+ items: result.items,
11879
+ idMap: result.idMap,
11880
+ workflows: result.workflows.map((w) => ({
11881
+ ref: w.ref,
11882
+ name: w.name,
11883
+ autoActions: w.nativeActions.length,
11884
+ manualSteps: w.manual,
11885
+ needsContent: w.needsContent,
11886
+ pendingRefs: w.pendingRefs,
11887
+ splitInto: w.splitInto,
11888
+ gatedBy: w.gatedBy,
11889
+ draft: w.gatedBy.length > 0 || !(publishWorkflows ?? false)
11890
+ })),
11891
+ calendarsManual: result.calendarsManual,
11892
+ handoffs: result.handoffs,
11893
+ report,
11894
+ 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.'
11895
+ });
11896
+ } catch (error) {
11897
+ return errorResponse(error);
11898
+ }
11899
+ }
11900
+ );
10925
11901
  if (!builderClient) return;
10926
11902
  const bc = builderClient;
10927
11903
  async function resolveCustomFields(locationId2, dryRun) {
@@ -10967,7 +11943,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
10967
11943
  resolved.set(q.key, rec);
10968
11944
  }
10969
11945
  if (!missing) break;
10970
- if (attempt < 6) await sleep(700 * attempt);
11946
+ if (attempt < 6) await sleep2(700 * attempt);
10971
11947
  }
10972
11948
  if (missing) {
10973
11949
  throw new Error(
@@ -11033,7 +12009,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
11033
12009
  break;
11034
12010
  } catch (saveErr) {
11035
12011
  if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
11036
- await sleep(700 * attempt);
12012
+ await sleep2(700 * attempt);
11037
12013
  continue;
11038
12014
  }
11039
12015
  throw saveErr;
@@ -11045,7 +12021,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
11045
12021
  const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
11046
12022
  persistedCount = countFormFields(verify);
11047
12023
  if (persistedCount > 0) break;
11048
- if (attempt < 6) await sleep(700 * attempt);
12024
+ if (attempt < 6) await sleep2(700 * attempt);
11049
12025
  }
11050
12026
  const fieldMap = {};
11051
12027
  for (const [key, rec] of resolved) fieldMap[key] = rec.id;
@@ -11367,8 +12343,8 @@ Subcommands:
11367
12343
 
11368
12344
  Exit codes: 0 ok, 2 usage, 3 validation failed, 4 filesystem write failed.
11369
12345
  Seed while the MCP server is stopped, or restart it afterwards.`;
11370
- function errLine(msg) {
11371
- process.stderr.write(msg + "\n");
12346
+ function errLine(msg2) {
12347
+ process.stderr.write(msg2 + "\n");
11372
12348
  }
11373
12349
  function preflightWritable() {
11374
12350
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.36.0",
3
+ "version": "3.38.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",