@elitedcs/ghl-mcp 3.44.0 → 3.46.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 CHANGED
@@ -31,9 +31,9 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.44.0",
34
+ version: "3.46.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
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.",
36
+ description: "GoHighLevel MCP Server for Claude. 220 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
38
38
  bin: {
39
39
  "ghl-mcp": "dist/index.js"
@@ -43,6 +43,8 @@ var require_package = __commonJS({
43
43
  "templates/action-schemas.json",
44
44
  "templates/clinic-medspa.json",
45
45
  "templates/trigger-schemas.json",
46
+ "templates/external-funnel/cloudflare-worker.js",
47
+ "templates/external-funnel/README.md",
46
48
  "README.md",
47
49
  "CHANGELOG.md"
48
50
  ],
@@ -2006,8 +2008,8 @@ function textResponse(text) {
2006
2008
  content: [{ type: "text", text }]
2007
2009
  };
2008
2010
  }
2009
- function escapeRegex(str) {
2010
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2011
+ function escapeRegex(str2) {
2012
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2011
2013
  }
2012
2014
  function errorMessage(error) {
2013
2015
  return error instanceof Error ? error.message : String(error);
@@ -6309,8 +6311,385 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
6309
6311
  );
6310
6312
  }
6311
6313
 
6312
- // src/tools/pipeline-builder.ts
6314
+ // src/tools/funnel-qa.ts
6313
6315
  var import_zod36 = require("zod");
6316
+ var asObj = (v) => v && typeof v === "object" ? v : {};
6317
+ var asArr = (v) => Array.isArray(v) ? v : [];
6318
+ var str = (v) => typeof v === "string" ? v : void 0;
6319
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6320
+ function rollupVerdict(checks) {
6321
+ const fail = checks.filter((c) => c.status === "fail");
6322
+ const warn = checks.filter((c) => c.status === "warn");
6323
+ const pass = checks.filter((c) => c.status === "pass");
6324
+ const verdict = fail.length ? "FAIL" : warn.length ? "PASS_WITH_WARNINGS" : "PASS";
6325
+ const summary = `${pass.length} pass, ${warn.length} warn, ${fail.length} fail.${fail.length ? " FAILS: " + fail.map((f) => f.label).join("; ") + "." : ""}`;
6326
+ return { ok: fail.length === 0, verdict, summary };
6327
+ }
6328
+ function parseContact(raw) {
6329
+ const c = asObj(asObj(raw).contact);
6330
+ const id = str(c.id) ?? str(c._id);
6331
+ if (!id) return void 0;
6332
+ const custom = /* @__PURE__ */ new Map();
6333
+ for (const f of asArr(c.customFields)) {
6334
+ const fo = asObj(f);
6335
+ const fid = str(fo.id) ?? str(fo.customFieldId) ?? str(fo.field_id);
6336
+ if (!fid) continue;
6337
+ const val = fo.value ?? fo.field_value ?? fo.fieldValue ?? "";
6338
+ custom.set(fid, Array.isArray(val) ? val.join(",") : String(val));
6339
+ }
6340
+ return {
6341
+ id,
6342
+ email: str(c.email)?.toLowerCase(),
6343
+ phone: str(c.phone),
6344
+ firstName: str(c.firstName),
6345
+ lastName: str(c.lastName),
6346
+ name: str(c.name),
6347
+ tags: asArr(c.tags).map((t) => String(t).toLowerCase()),
6348
+ source: str(c.source),
6349
+ assignedTo: str(c.assignedTo),
6350
+ custom
6351
+ };
6352
+ }
6353
+ async function findContactsByEmail(client, locationId2, email) {
6354
+ const raw = await client.get("/contacts/", { params: { locationId: locationId2, query: email, limit: 20 } });
6355
+ const want = email.toLowerCase();
6356
+ const ids = [];
6357
+ for (const c of asArr(asObj(raw).contacts)) {
6358
+ const co = asObj(c);
6359
+ if (str(co.email)?.toLowerCase() === want) {
6360
+ const id = str(co.id) ?? str(co._id);
6361
+ if (id) ids.push(id);
6362
+ }
6363
+ }
6364
+ return ids;
6365
+ }
6366
+ async function getContact(client, contactId) {
6367
+ return parseContact(await client.get(`/contacts/${contactId}`));
6368
+ }
6369
+ async function settledEmailIds(client, locationId2, email, opts = {}) {
6370
+ const tries = opts.tries ?? 6;
6371
+ const delayMs = opts.delayMs ?? 3500;
6372
+ const seen = /* @__PURE__ */ new Set();
6373
+ for (let i = 1; i <= tries; i++) {
6374
+ for (const id of await findContactsByEmail(client, locationId2, email)) seen.add(id);
6375
+ if (seen.size >= 2) break;
6376
+ if (i < tries) await sleep(delayMs);
6377
+ }
6378
+ return [...seen];
6379
+ }
6380
+ async function hasSmsCapability(client, locationId2) {
6381
+ let numbers = 0;
6382
+ let pools = 0;
6383
+ try {
6384
+ numbers = asArr(asObj(await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } })).phoneNumbers).length;
6385
+ } catch {
6386
+ }
6387
+ try {
6388
+ pools = asArr(asObj(await client.get("/phone-system/number-pools/", { params: { locationId: locationId2 } })).pools).length;
6389
+ } catch {
6390
+ }
6391
+ return { numbers, pools };
6392
+ }
6393
+ async function latestOutboundMs(client, locationId2, contactId) {
6394
+ const convRaw = await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } });
6395
+ const conversations = asArr(asObj(convRaw).conversations);
6396
+ let best;
6397
+ for (const c of conversations) {
6398
+ const convId = str(asObj(c).id) ?? str(asObj(c)._id);
6399
+ if (!convId) continue;
6400
+ let msgsRaw;
6401
+ try {
6402
+ msgsRaw = await client.get(`/conversations/${convId}/messages`, { params: { limit: 50 } });
6403
+ } catch {
6404
+ continue;
6405
+ }
6406
+ const mo = asObj(msgsRaw);
6407
+ const list = asArr(mo.messages).length ? asArr(mo.messages) : asArr(asObj(mo.messages).messages);
6408
+ for (const m of list) {
6409
+ const msg2 = asObj(m);
6410
+ const dir = str(msg2.direction)?.toLowerCase();
6411
+ if (dir !== "outbound") continue;
6412
+ const at = Date.parse(str(msg2.dateAdded) ?? str(msg2.dateUpdated) ?? "");
6413
+ const atMs = Number.isNaN(at) ? void 0 : at;
6414
+ if (!best || atMs && (!best.atMs || atMs > best.atMs)) best = { type: str(msg2.type), atMs };
6415
+ }
6416
+ }
6417
+ return best ? { found: true, ...best } : { found: false };
6418
+ }
6419
+ async function readWorkflow(bc, workflowId) {
6420
+ try {
6421
+ const full = asObj(await bc.getWorkflow(workflowId));
6422
+ const status = str(full.status);
6423
+ const templates = asArr(asObj(full.workflowData).templates);
6424
+ const sendsSms = templates.some((t) => str(asObj(t).type) === "sms");
6425
+ return { status, sendsSms };
6426
+ } catch {
6427
+ return void 0;
6428
+ }
6429
+ }
6430
+ async function submit(url, body) {
6431
+ const res = await fetch(url, {
6432
+ method: "POST",
6433
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
6434
+ body: JSON.stringify(body)
6435
+ });
6436
+ const text = (await res.text().catch(() => "")).slice(0, 300);
6437
+ return { httpStatus: res.status, blockedByCaptcha: res.status === 403 && /captcha|turnstile/i.test(text), text };
6438
+ }
6439
+ var customFieldInput = import_zod36.z.object({
6440
+ fieldId: import_zod36.z.string().describe("The VERIFIED GHL custom-field id the form sends (from apply_build_plan's wiring bundle)."),
6441
+ value: import_zod36.z.string().describe("The value the form sends for this field \u2014 asserted to PERSIST on the contact."),
6442
+ label: import_zod36.z.string().optional().describe("Human label (informational).")
6443
+ });
6444
+ function registerFunnelQaTools(server2, client, builderClient) {
6445
+ server2.tool(
6446
+ "verify_funnel",
6447
+ "Prove a live public funnel actually captures a lead AND the automation acts on it \u2014 the runtime companion to audit_workflows. Submits a sentinel lead to funnelUrl, then reads GHL back to verify what REALLY happened (the UI/thank-you page is never proof): (1) backend contact truth \u2014 the contact is actually created (search_contacts), with submit\u2192appear latency; (2) field-value fidelity \u2014 each expectCustom value actually PERSISTED on the contact (a wrong field id makes upsert succeed while silently dropping the value); (3) the triggerTag landed (else the workflow can't fire); (4) SMS/A2P pre-check \u2014 flags if the account has no SMS-capable number so any SMS step would silently fail; (5) workflow status \u2014 flags a DRAFT workflow (never fires on real leads); (6) outreach fired \u2014 an OUTBOUND message was actually logged for the contact (proves enrollment + send, not just a green log), with latency; (7) consent recorded when messaging fires (TCPA/CAN-SPAM); (8) duplicate-contact retest \u2014 a second identical submit dedups to ONE contact, not a duplicate/false-failure; (9) opportunity/routing (expectOpportunity/expectAssigned/expectSource); (10) multi-surface (extraUrls) + concurrent double-submit race. Booking can't be faked server-side, so a real burner appointment is surfaced as a manual step. WARNING: this creates a real (clearly-marked 'blueprint-qa') contact and MAY fire real automation + internal notifications; it deletes the sentinel afterward by default (cleanup). Run it on the REAL production URL.",
6448
+ {
6449
+ funnelUrl: import_zod36.z.string().describe("The live URL the test submission POSTs to (your external lead-bridge Worker URL, or any endpoint that creates the GHL contact). POSTed as JSON."),
6450
+ sentinelEmail: import_zod36.z.string().optional().describe("Override the minted sentinel email. Default: a unique blueprint-qa.<id>@example.com (won't deliver; used only to find the contact)."),
6451
+ testPhone: import_zod36.z.string().optional().describe("Include a phone on the submission (enables SMS-path checks). Must be a test-safe number you control \u2014 real SMS may send + bill."),
6452
+ submitBody: import_zod36.z.record(import_zod36.z.unknown()).optional().describe("Full JSON body to POST (advanced; for non-bridge endpoints). The sentinel email is injected if absent. Default body is the lead-bridge contract {first_name,last_name,email,phone,custom,_hp}."),
6453
+ expectCustom: import_zod36.z.array(customFieldInput).optional().describe("Custom fields the form sends \u2014 each is asserted to PERSIST with the sent value (field-value fidelity, Carlos #3)."),
6454
+ expectStandard: import_zod36.z.array(import_zod36.z.string()).optional().describe("Standard contact keys to assert saved (e.g. ['phone','first_name']). email is always asserted."),
6455
+ triggerTag: import_zod36.z.string().optional().describe("The tag the form/bridge should add (the speed-to-lead trigger). Asserted to land on the contact."),
6456
+ workflowId: import_zod36.z.string().optional().describe("The speed-to-lead workflow to inspect \u2014 flags DRAFT status and cross-checks SMS capability."),
6457
+ consentFieldId: import_zod36.z.string().optional().describe("Custom field id that records consent \u2014 asserted populated when messaging fires (legal)."),
6458
+ expectOutreach: import_zod36.z.boolean().optional().describe("Poll the message log for an OUTBOUND message to the contact (proves the automation fired + sent). Defaults true when triggerTag or workflowId is given."),
6459
+ outreachWaitSec: import_zod36.z.number().optional().describe("How long to poll for the outbound message (default 90s)."),
6460
+ expectOpportunity: import_zod36.z.object({ pipelineId: import_zod36.z.string().optional(), stageId: import_zod36.z.string().optional() }).optional().describe("Assert an opportunity was created for the contact (optionally in this pipeline/stage)."),
6461
+ expectAssigned: import_zod36.z.boolean().optional().describe("Assert the contact was assigned to a user (someone follows up)."),
6462
+ expectSource: import_zod36.z.boolean().optional().describe("Assert a lead source/attribution was captured (reporting works)."),
6463
+ extraUrls: import_zod36.z.array(import_zod36.z.string()).optional().describe("Additional capture surfaces to test the same way (each gets its own sentinel) \u2014 popup/inline/exit-intent forms."),
6464
+ testConcurrentDuplicate: import_zod36.z.boolean().optional().describe("Fire two simultaneous submits with a fresh email \u2192 assert exactly ONE contact (concurrent dedup race)."),
6465
+ cleanup: import_zod36.z.boolean().optional().describe("Delete the sentinel contact(s) after verifying. Default true."),
6466
+ locationId: import_zod36.z.string().optional().describe("Target sub-account. Must match the active location.")
6467
+ },
6468
+ async (args) => {
6469
+ try {
6470
+ const locationId2 = client.resolveLocationId(args.locationId);
6471
+ if (args.locationId && args.locationId !== locationId2) {
6472
+ return jsonResponse({ ok: false, error: `Location mismatch: active is ${locationId2}, you passed ${args.locationId}. switch_location + confirm first.` });
6473
+ }
6474
+ const checks = [];
6475
+ const cleanup = args.cleanup !== false;
6476
+ const sentinels = [];
6477
+ const token = crypto.randomUUID().slice(0, 8);
6478
+ const sentinelEmail = (args.sentinelEmail ?? `blueprint-qa.${token}@example.com`).toLowerCase();
6479
+ const custom = {};
6480
+ for (const f of args.expectCustom ?? []) custom[f.fieldId] = f.value;
6481
+ const body = args.submitBody ? { ...args.submitBody } : { first_name: "Blueprint", last_name: `QA ${token}`, custom, _hp: "" };
6482
+ if (typeof body.email !== "string" || !body.email) body.email = sentinelEmail;
6483
+ const searchEmail = String(body.email).toLowerCase();
6484
+ if (args.testPhone && !body.phone) body.phone = args.testPhone;
6485
+ const submitMs = Date.now();
6486
+ let httpInfo = "";
6487
+ try {
6488
+ const r = await submit(args.funnelUrl, body);
6489
+ httpInfo = `HTTP ${r.httpStatus}`;
6490
+ if (r.blockedByCaptcha) {
6491
+ checks.push({ id: "spam_gate", label: "Spam gate (Turnstile)", status: "warn", detail: `The funnel's Turnstile gate blocked this automated submit (${httpInfo}). That's correct protection \u2014 but it means verify_funnel can't drive this path. Test it manually, or temporarily allow a QA bypass.` });
6492
+ }
6493
+ } catch (e) {
6494
+ checks.push({ id: "submit", label: "Submit reached the endpoint", status: "fail", detail: `POST to funnelUrl failed: ${e instanceof Error ? e.message : String(e)}` });
6495
+ }
6496
+ let contactId;
6497
+ let cleanedUp = false;
6498
+ try {
6499
+ let appearMs;
6500
+ const backendTries = 12;
6501
+ for (let i = 1; i <= backendTries; i++) {
6502
+ const ids = await findContactsByEmail(client, locationId2, searchEmail);
6503
+ if (ids.length) {
6504
+ contactId = ids[0];
6505
+ appearMs = Date.now();
6506
+ break;
6507
+ }
6508
+ if (i < backendTries) await sleep(2e3 + i * 500);
6509
+ }
6510
+ const sms = await hasSmsCapability(client, locationId2);
6511
+ const wf = args.workflowId && builderClient ? await readWorkflow(builderClient, args.workflowId) : void 0;
6512
+ if (!contactId) {
6513
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "fail", detail: `No contact with ${searchEmail} appeared in GHL after ~${Math.round(backendTries * 2.5)}s (${httpInfo || "submit failed"}). The funnel is a paper shredder \u2014 leads evaporate even if the page says thanks.` });
6514
+ } else {
6515
+ sentinels.push(contactId);
6516
+ const latencySec = appearMs ? Math.round((appearMs - submitMs) / 1e3) : void 0;
6517
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "pass", detail: `Contact ${contactId} created (${httpInfo}). Submit\u2192appear ~${latencySec}s.` });
6518
+ checks.push(latencySec !== void 0 && latencySec > 60 ? { id: "latency", label: "Speed-to-lead latency", status: "warn", detail: `Lead took ~${latencySec}s to land \u2014 slow capture loses leads. Investigate the bridge/retry path.` } : { id: "latency", label: "Speed-to-lead latency", status: "pass", detail: `~${latencySec ?? "<1"}s submit\u2192contact.` });
6519
+ const contact = await getContact(client, contactId);
6520
+ if (!contact) {
6521
+ checks.push({ id: "contact_read", label: "Contact readable", status: "fail", detail: `Contact ${contactId} was found by search but get_contact returned nothing \u2014 cannot verify field values, tags, consent, or routing.` });
6522
+ }
6523
+ if (args.expectCustom?.length || args.expectStandard?.length) {
6524
+ if (!contact) {
6525
+ checks.push({ id: "field_fidelity", label: "Field-value persistence", status: "fail", detail: "Could not read the contact to verify field values." });
6526
+ } else {
6527
+ const probs = [];
6528
+ const notes = [];
6529
+ for (const f of args.expectCustom ?? []) {
6530
+ const got2 = contact.custom.get(f.fieldId);
6531
+ if (got2 === void 0) probs.push(`custom ${f.label ?? f.fieldId}: NOT saved (wrong field id? upsert dropped it silently)`);
6532
+ else if (got2.trim() !== f.value.trim()) probs.push(`custom ${f.label ?? f.fieldId}: saved "${got2}" \u2260 sent "${f.value}"`);
6533
+ }
6534
+ const submitted = {
6535
+ email: str(body.email)?.toLowerCase(),
6536
+ phone: str(body.phone),
6537
+ first_name: str(body.first_name),
6538
+ last_name: str(body.last_name),
6539
+ name: str(body.name)
6540
+ };
6541
+ const got = {
6542
+ email: contact.email,
6543
+ phone: contact.phone,
6544
+ first_name: contact.firstName,
6545
+ last_name: contact.lastName,
6546
+ name: contact.name
6547
+ };
6548
+ const same = (key, a, b) => key === "phone" ? (a ?? "").replace(/\D/g, "") === (b ?? "").replace(/\D/g, "") : (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
6549
+ for (const k of args.expectStandard ?? []) {
6550
+ const key = k.toLowerCase();
6551
+ if (!(key in got)) {
6552
+ notes.push(`standard ${k}: not a verifiable standard key`);
6553
+ continue;
6554
+ }
6555
+ const exp = submitted[key];
6556
+ if (exp === void 0 || exp === "") {
6557
+ notes.push(`standard ${k}: not part of the submission, value not verified`);
6558
+ continue;
6559
+ }
6560
+ if (!got[key]) probs.push(`standard ${k}: not saved`);
6561
+ else if (!same(key, got[key], exp)) probs.push(`standard ${k}: saved "${got[key]}" \u2260 sent "${exp}"`);
6562
+ }
6563
+ const okCount = (args.expectCustom?.length ?? 0) + (args.expectStandard?.length ?? 0) - probs.length - notes.length;
6564
+ checks.push(probs.length ? { id: "field_fidelity", label: "Field-value persistence", status: "fail", detail: [probs.join("; "), notes.join("; ")].filter(Boolean).join(" | ") } : { id: "field_fidelity", label: "Field-value persistence", status: "pass", detail: `${okCount} asserted field value(s) persisted with the exact submitted value.${notes.length ? " " + notes.join("; ") : ""}` });
6565
+ }
6566
+ } else {
6567
+ checks.push({ id: "field_fidelity", label: "Field-value persistence", status: "skip", detail: "No expectCustom/expectStandard given \u2014 pass them (with the wiring bundle's verified field ids) to prove values persist." });
6568
+ }
6569
+ if (args.triggerTag) {
6570
+ const want = args.triggerTag.toLowerCase();
6571
+ if (!contact) checks.push({ id: "trigger_tag", label: "Trigger tag landed", status: "fail", detail: "Could not read the contact to verify the trigger tag." });
6572
+ else checks.push(contact.tags.includes(want) ? { id: "trigger_tag", label: "Trigger tag landed", status: "pass", detail: `Tag "${args.triggerTag}" is on the contact \u2014 the speed-to-lead workflow can fire.` } : { id: "trigger_tag", label: "Trigger tag landed", status: "fail", detail: `Tag "${args.triggerTag}" is NOT on the contact (tags: ${contact.tags.join(", ") || "none"}). The workflow won't fire \u2014 check the tag spelling/case matches the trigger exactly.` });
6573
+ }
6574
+ if (sms.numbers === 0 && sms.pools === 0) {
6575
+ const willSend = wf?.sendsSms;
6576
+ checks.push({ id: "sms_capability", label: "SMS / A2P capability", status: willSend ? "fail" : "warn", detail: `No SMS-capable number or pool in this account.${willSend ? " The workflow HAS an SMS step \u2192 it will silently fail." : " Any SMS step in any workflow will silently fail (send to no one)."} Provision a number + approve A2P.` });
6577
+ } else {
6578
+ checks.push({ id: "sms_capability", label: "SMS / A2P capability", status: "pass", detail: `${sms.numbers} number(s), ${sms.pools} pool(s) provisioned.` });
6579
+ }
6580
+ if (args.workflowId) {
6581
+ if (!builderClient) checks.push({ id: "workflow_status", label: "Workflow is published", status: "skip", detail: "Workflow status needs the workflow-builder (Firebase) client, which isn't configured on this install." });
6582
+ else if (!wf) checks.push({ id: "workflow_status", label: "Workflow is published", status: "warn", detail: `Could not read workflow ${args.workflowId} (id wrong, or not readable).` });
6583
+ else if (str(wf.status)?.toLowerCase() === "draft") checks.push({ id: "workflow_status", label: "Workflow is published", status: "fail", detail: `Workflow ${args.workflowId} is DRAFT \u2014 it will NOT fire on real leads. Publish it.` });
6584
+ else checks.push({ id: "workflow_status", label: "Workflow is published", status: "pass", detail: `Workflow status: ${wf.status}.` });
6585
+ }
6586
+ const wantOutreach = args.expectOutreach ?? !!(args.triggerTag || args.workflowId);
6587
+ if (wantOutreach) {
6588
+ const waitSec = args.outreachWaitSec ?? 90;
6589
+ const deadline = Date.now() + waitSec * 1e3;
6590
+ let out = await latestOutboundMs(client, locationId2, contactId);
6591
+ while (!out.found && Date.now() < deadline) {
6592
+ await sleep(5e3);
6593
+ out = await latestOutboundMs(client, locationId2, contactId);
6594
+ }
6595
+ checks.push(out.found ? { id: "outreach_fired", label: "Speed-to-lead outreach sent", status: "pass", detail: `An outbound ${out.type ?? "message"} was logged for the contact${out.atMs ? ` (~${Math.max(0, Math.round((out.atMs - submitMs) / 1e3))}s after submit)` : ""} \u2014 automation fired and sent.` } : { id: "outreach_fired", label: "Speed-to-lead outreach sent", status: "fail", detail: `No outbound message logged within ${waitSec}s. The workflow may not have enrolled (draft? tag mismatch? re-enrollment off) or the send silently failed (no number/A2P, unverified email domain, broken merge field).` });
6596
+ }
6597
+ if (args.consentFieldId) {
6598
+ const messagingFires = (args.expectOutreach ?? !!(args.triggerTag || args.workflowId)) || wf?.sendsSms;
6599
+ if (!contact) checks.push({ id: "consent", label: "Consent recorded", status: "fail", detail: "Could not read the contact to verify consent." });
6600
+ else {
6601
+ const v = contact.custom.get(args.consentFieldId);
6602
+ checks.push(v && v.trim() !== "" ? { id: "consent", label: "Consent recorded", status: "pass", detail: `Consent field populated ("${v}").` } : { id: "consent", label: "Consent recorded", status: messagingFires ? "fail" : "warn", detail: `Consent field ${args.consentFieldId} is empty${messagingFires ? " while the funnel sends marketing messages \u2014 TCPA/CAN-SPAM exposure. Collect + record explicit consent." : "."}` });
6603
+ }
6604
+ }
6605
+ if (args.expectOpportunity) {
6606
+ try {
6607
+ const oppRaw = await client.get("/opportunities/search", { params: { location_id: locationId2, contact_id: contactId } });
6608
+ const opps = asArr(asObj(oppRaw).opportunities).map(asObj);
6609
+ if (opps.length === 0) {
6610
+ checks.push({ id: "opportunity", label: "Opportunity created", status: "fail", detail: "No opportunity exists for the contact \u2014 the create-opportunity step didn't run (often a dead pipeline/stage id silently kills it)." });
6611
+ } else {
6612
+ const want = args.expectOpportunity;
6613
+ const match = opps.find((o) => (!want.pipelineId || str(o.pipelineId) === want.pipelineId) && (!want.stageId || str(o.pipelineStageId) === want.stageId));
6614
+ checks.push(match ? { id: "opportunity", label: "Opportunity created", status: "pass", detail: `Opportunity in the expected pipeline/stage.` } : { id: "opportunity", label: "Opportunity created", status: "fail", detail: `An opportunity exists but not in the expected pipeline/stage (got pipeline ${str(opps[0].pipelineId)}, stage ${str(opps[0].pipelineStageId)}).` });
6615
+ }
6616
+ } catch (e) {
6617
+ checks.push({ id: "opportunity", label: "Opportunity created", status: "warn", detail: `Could not read opportunities: ${e instanceof Error ? e.message : String(e)}` });
6618
+ }
6619
+ }
6620
+ if (args.expectAssigned) {
6621
+ if (!contact) checks.push({ id: "routing", label: "Contact assigned to a rep", status: "fail", detail: "Could not read the contact to verify assignment." });
6622
+ else checks.push(contact.assignedTo ? { id: "routing", label: "Contact assigned to a rep", status: "pass", detail: `Assigned to ${contact.assignedTo}.` } : { id: "routing", label: "Contact assigned to a rep", status: "fail", detail: "Contact is unassigned \u2014 nobody owns follow-up." });
6623
+ }
6624
+ if (args.expectSource) {
6625
+ if (!contact) checks.push({ id: "attribution", label: "Lead source captured", status: "fail", detail: "Could not read the contact to verify source." });
6626
+ else checks.push(contact.source ? { id: "attribution", label: "Lead source captured", status: "pass", detail: `source = "${contact.source}".` } : { id: "attribution", label: "Lead source captured", status: "warn", detail: "No source/attribution on the contact \u2014 reporting can't tell which funnel/ad produced it." });
6627
+ }
6628
+ await submit(args.funnelUrl, body).catch(() => void 0);
6629
+ const dupIds = await settledEmailIds(client, locationId2, searchEmail, { tries: 6, delayMs: 3e3 });
6630
+ for (const id of dupIds) if (!sentinels.includes(id)) sentinels.push(id);
6631
+ checks.push(dupIds.length === 1 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "pass", detail: "A second identical submit deduped to ONE contact (upsert by email)." } : dupIds.length === 0 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "warn", detail: `Could not observe the contact for ${searchEmail} on re-read (search lag) \u2014 dedup not conclusively verified.` } : { id: "duplicate", label: "Duplicate-contact dedup", status: "fail", detail: `A second submit produced ${dupIds.length} contacts for ${searchEmail} \u2014 dedup is broken (real returning visitors will duplicate or false-fail).` });
6632
+ if (args.testConcurrentDuplicate) {
6633
+ const raceToken = crypto.randomUUID().slice(0, 8);
6634
+ const raceEmail = `blueprint-qa.${raceToken}@example.com`;
6635
+ const raceBody = { ...body, email: raceEmail, last_name: `QA race ${raceToken}` };
6636
+ await Promise.allSettled([submit(args.funnelUrl, raceBody), submit(args.funnelUrl, raceBody)]);
6637
+ const raceIds = await settledEmailIds(client, locationId2, raceEmail, { tries: 6, delayMs: 3e3 });
6638
+ for (const id of raceIds) if (!sentinels.includes(id)) sentinels.push(id);
6639
+ checks.push(raceIds.length === 1 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "pass", detail: `Two simultaneous submits \u2192 1 contact (no race duplicate).` } : raceIds.length === 0 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "warn", detail: "Could not observe the race result (search lag, or neither submit landed)." } : { id: "concurrent_race", label: "Concurrent double-submit", status: "fail", detail: `Two simultaneous submits created ${raceIds.length} contacts \u2014 a concurrency race duplicates leads under real traffic.` });
6640
+ }
6641
+ for (const url of args.extraUrls ?? []) {
6642
+ const sToken = crypto.randomUUID().slice(0, 8);
6643
+ const sEmail = `blueprint-qa.${sToken}@example.com`;
6644
+ const sBody = { ...body, email: sEmail, last_name: `QA ${sToken}` };
6645
+ let ids = [];
6646
+ try {
6647
+ await submit(url, sBody);
6648
+ ids = await settledEmailIds(client, locationId2, sEmail, { tries: 8, delayMs: 2500 });
6649
+ } catch {
6650
+ }
6651
+ for (const id of ids) if (!sentinels.includes(id)) sentinels.push(id);
6652
+ checks.push(ids.length >= 1 ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "pass", detail: `Submission landed ${ids.length} contact(s).` } : { id: `surface:${url}`, label: `Capture surface ${url}`, status: "fail", detail: "Submission did NOT land a contact \u2014 this surface is a paper shredder." });
6653
+ }
6654
+ }
6655
+ checks.push({ id: "booking", label: "Booking (manual)", status: "skip", detail: "A reachable calendar URL is not proof. Book ONE real test ('burner') appointment through the live booking widget and confirm it lands in GHL (Carlos #6). This step can't be verified server-side." });
6656
+ } catch (e) {
6657
+ checks.push({ id: "error", label: "verify_funnel run error", status: "fail", detail: e instanceof Error ? e.message : String(e) });
6658
+ } finally {
6659
+ const uniq = [...new Set(sentinels)];
6660
+ if (!cleanup) {
6661
+ checks.push({ id: "cleanup", label: "Sentinel cleanup", status: "skip", detail: `cleanup:false \u2014 ${uniq.length} "blueprint-qa" sentinel contact(s) left in the account. Delete them so QA doesn't pollute reporting.` });
6662
+ } else if (uniq.length === 0) {
6663
+ cleanedUp = true;
6664
+ checks.push({ id: "cleanup", label: "Sentinel cleanup", status: "skip", detail: "No sentinel contacts were created." });
6665
+ } else {
6666
+ let deleted = 0;
6667
+ for (const id of uniq) {
6668
+ try {
6669
+ await client.delete(`/contacts/${id}`);
6670
+ deleted++;
6671
+ } catch {
6672
+ }
6673
+ }
6674
+ cleanedUp = deleted === uniq.length;
6675
+ checks.push({ id: "cleanup", label: "Sentinel cleanup", status: cleanedUp ? "pass" : "warn", detail: cleanedUp ? `Deleted ${deleted} sentinel contact(s).` : `Deleted ${deleted}/${uniq.length} sentinel contact(s) \u2014 remove any leftover "blueprint-qa" contacts manually.` });
6676
+ }
6677
+ }
6678
+ return finish(checks, { contactId, sentinelEmail: searchEmail, cleanedUp });
6679
+ } catch (error) {
6680
+ return errorResponse(error);
6681
+ }
6682
+ }
6683
+ );
6684
+ }
6685
+ function finish(checks, extra) {
6686
+ const { ok, verdict, summary } = rollupVerdict(checks);
6687
+ const note = "WARNING: verify_funnel created a real (clearly-marked 'blueprint-qa') contact and may have fired real automation + internal notifications. Run on the real production URL; the form/thank-you page is never proof \u2014 only the backend is.";
6688
+ return jsonResponse({ ok, verdict, summary, checks, note, ...extra });
6689
+ }
6690
+
6691
+ // src/tools/pipeline-builder.ts
6692
+ var import_zod37 = require("zod");
6314
6693
  function registerPipelineBuilderTools(server2, builderClient) {
6315
6694
  const client = builderClient;
6316
6695
  if (!client) return;
@@ -6355,7 +6734,7 @@ ${text2}`);
6355
6734
  "get_pipeline_full",
6356
6735
  "Get a single pipeline with complete stage configuration: IDs, names, positions, display settings.",
6357
6736
  {
6358
- pipelineId: import_zod36.z.string().describe("The pipeline ID to retrieve.")
6737
+ pipelineId: import_zod37.z.string().describe("The pipeline ID to retrieve.")
6359
6738
  },
6360
6739
  async ({ pipelineId }) => {
6361
6740
  try {
@@ -6373,17 +6752,17 @@ ${text2}`);
6373
6752
  "create_pipeline",
6374
6753
  "Create a new pipeline with stages. Each stage needs a name and position (0-based).",
6375
6754
  {
6376
- name: import_zod36.z.string().describe("Pipeline name."),
6377
- stages: import_zod36.z.array(
6378
- import_zod36.z.object({
6379
- name: import_zod36.z.string().describe("Stage name."),
6380
- position: import_zod36.z.number().describe("Stage position (0-based)."),
6381
- showInFunnel: import_zod36.z.boolean().optional().describe("Show in funnel view. Defaults to true."),
6382
- showInPieChart: import_zod36.z.boolean().optional().describe("Show in pie chart. Defaults to true.")
6755
+ name: import_zod37.z.string().describe("Pipeline name."),
6756
+ stages: import_zod37.z.array(
6757
+ import_zod37.z.object({
6758
+ name: import_zod37.z.string().describe("Stage name."),
6759
+ position: import_zod37.z.number().describe("Stage position (0-based)."),
6760
+ showInFunnel: import_zod37.z.boolean().optional().describe("Show in funnel view. Defaults to true."),
6761
+ showInPieChart: import_zod37.z.boolean().optional().describe("Show in pie chart. Defaults to true.")
6383
6762
  })
6384
6763
  ).describe("Array of stages in order."),
6385
- showInFunnel: import_zod36.z.boolean().optional().describe("Show pipeline in funnel view. Defaults to true."),
6386
- showInPieChart: import_zod36.z.boolean().optional().describe("Show pipeline in pie chart. Defaults to true.")
6764
+ showInFunnel: import_zod37.z.boolean().optional().describe("Show pipeline in funnel view. Defaults to true."),
6765
+ showInPieChart: import_zod37.z.boolean().optional().describe("Show pipeline in pie chart. Defaults to true.")
6387
6766
  },
6388
6767
  async ({ name, stages, showInFunnel, showInPieChart }) => {
6389
6768
  try {
@@ -6413,19 +6792,19 @@ ${text2}`);
6413
6792
  "update_pipeline",
6414
6793
  "Update a pipeline's name, stages, or display settings. You can add, remove, rename, or reorder stages. Pass the complete stages array \u2014 stages not included will be removed.",
6415
6794
  {
6416
- pipelineId: import_zod36.z.string().describe("The pipeline ID to update."),
6417
- name: import_zod36.z.string().optional().describe("New pipeline name."),
6418
- stages: import_zod36.z.array(
6419
- import_zod36.z.object({
6420
- id: import_zod36.z.string().optional().describe("Existing stage ID (omit for new stages)."),
6421
- name: import_zod36.z.string().describe("Stage name."),
6422
- position: import_zod36.z.number().describe("Stage position (0-based)."),
6423
- showInFunnel: import_zod36.z.boolean().optional().describe("Show in funnel view."),
6424
- showInPieChart: import_zod36.z.boolean().optional().describe("Show in pie chart.")
6795
+ pipelineId: import_zod37.z.string().describe("The pipeline ID to update."),
6796
+ name: import_zod37.z.string().optional().describe("New pipeline name."),
6797
+ stages: import_zod37.z.array(
6798
+ import_zod37.z.object({
6799
+ id: import_zod37.z.string().optional().describe("Existing stage ID (omit for new stages)."),
6800
+ name: import_zod37.z.string().describe("Stage name."),
6801
+ position: import_zod37.z.number().describe("Stage position (0-based)."),
6802
+ showInFunnel: import_zod37.z.boolean().optional().describe("Show in funnel view."),
6803
+ showInPieChart: import_zod37.z.boolean().optional().describe("Show in pie chart.")
6425
6804
  })
6426
6805
  ).optional().describe("Complete stages array. Stages not included will be removed."),
6427
- showInFunnel: import_zod36.z.boolean().optional().describe("Show pipeline in funnel view."),
6428
- showInPieChart: import_zod36.z.boolean().optional().describe("Show pipeline in pie chart.")
6806
+ showInFunnel: import_zod37.z.boolean().optional().describe("Show pipeline in funnel view."),
6807
+ showInPieChart: import_zod37.z.boolean().optional().describe("Show pipeline in pie chart.")
6429
6808
  },
6430
6809
  async ({ pipelineId, name, stages, showInFunnel, showInPieChart }) => {
6431
6810
  try {
@@ -6448,8 +6827,8 @@ ${text2}`);
6448
6827
  "delete_pipeline",
6449
6828
  "Permanently delete a pipeline and all its stages. Opportunities become unassigned. IRREVERSIBLE.",
6450
6829
  {
6451
- pipelineId: import_zod36.z.string().describe("The pipeline ID to delete."),
6452
- confirm: import_zod36.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action.")
6830
+ pipelineId: import_zod37.z.string().describe("The pipeline ID to delete."),
6831
+ confirm: import_zod37.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action.")
6453
6832
  },
6454
6833
  async ({ pipelineId }) => {
6455
6834
  try {
@@ -6466,12 +6845,12 @@ ${text2}`);
6466
6845
  }
6467
6846
 
6468
6847
  // src/tools/location-switcher.ts
6469
- var import_zod38 = require("zod");
6848
+ var import_zod39 = require("zod");
6470
6849
 
6471
6850
  // src/setup-tool.ts
6472
6851
  var os2 = __toESM(require("os"));
6473
6852
  var crypto2 = __toESM(require("crypto"));
6474
- var import_zod37 = require("zod");
6853
+ var import_zod38 = require("zod");
6475
6854
 
6476
6855
  // src/firebase-capture-script.ts
6477
6856
  var FIREBASE_CAPTURE_SCRIPT = `(async () => {
@@ -6646,19 +7025,19 @@ function registerSetupTool(server2) {
6646
7025
  "setup_ghl_mcp",
6647
7026
  "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 212 tools (163 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
6648
7027
  {
6649
- email: import_zod37.z.string().email().describe("Email used at purchase."),
6650
- license_key: import_zod37.z.string().min(20).describe("License key from your purchase email."),
6651
- ghl_api_key: import_zod37.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
6652
- ghl_location_id: import_zod37.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
6653
- ghl_company_id: import_zod37.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
7028
+ email: import_zod38.z.string().email().describe("Email used at purchase."),
7029
+ license_key: import_zod38.z.string().min(20).describe("License key from your purchase email."),
7030
+ ghl_api_key: import_zod38.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
7031
+ ghl_location_id: import_zod38.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
7032
+ ghl_company_id: import_zod38.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
6654
7033
  // v3.25.0: one-paste shortcut. Run `auto_capture_firebase_script` first;
6655
7034
  // it returns a console script that fills the clipboard with this exact
6656
7035
  // JSON payload. Pasting it here removes the need to fill ghl_user_id,
6657
7036
  // ghl_firebase_api_key, and ghl_firebase_refresh_token individually.
6658
- firebase_paste: import_zod37.z.string().optional().describe("(Workflow Builder, one-paste path) Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
6659
- ghl_user_id: import_zod37.z.string().optional().describe("(Workflow Builder, manual path) Firebase User ID. Prefer firebase_paste instead."),
6660
- ghl_firebase_api_key: import_zod37.z.string().optional().describe("(Workflow Builder, manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste instead."),
6661
- ghl_firebase_refresh_token: import_zod37.z.string().optional().describe("(Workflow Builder, manual path) Firebase refresh token. Prefer firebase_paste instead.")
7037
+ firebase_paste: import_zod38.z.string().optional().describe("(Workflow Builder, one-paste path) Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
7038
+ ghl_user_id: import_zod38.z.string().optional().describe("(Workflow Builder, manual path) Firebase User ID. Prefer firebase_paste instead."),
7039
+ ghl_firebase_api_key: import_zod38.z.string().optional().describe("(Workflow Builder, manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste instead."),
7040
+ ghl_firebase_refresh_token: import_zod38.z.string().optional().describe("(Workflow Builder, manual path) Firebase refresh token. Prefer firebase_paste instead.")
6662
7041
  },
6663
7042
  async (args) => {
6664
7043
  const lic = await validateLicense(args.email, args.license_key);
@@ -6759,10 +7138,10 @@ function registerEnableWorkflowBuilderTool(server2) {
6759
7138
  // get the console script; the script returns a JSON object that pastes
6760
7139
  // cleanly into this field. Saves the buyer from picking out three
6761
7140
  // separate fields in IndexedDB.
6762
- firebase_paste: import_zod37.z.string().optional().describe("Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
6763
- ghl_user_id: import_zod37.z.string().min(10).optional().describe("(Manual path) Firebase User ID (uid). Prefer firebase_paste."),
6764
- ghl_firebase_api_key: import_zod37.z.string().min(10).optional().describe("(Manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste."),
6765
- ghl_firebase_refresh_token: import_zod37.z.string().min(10).optional().describe("(Manual path) Firebase refresh token. Prefer firebase_paste.")
7141
+ firebase_paste: import_zod38.z.string().optional().describe("Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
7142
+ ghl_user_id: import_zod38.z.string().min(10).optional().describe("(Manual path) Firebase User ID (uid). Prefer firebase_paste."),
7143
+ ghl_firebase_api_key: import_zod38.z.string().min(10).optional().describe("(Manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste."),
7144
+ ghl_firebase_refresh_token: import_zod38.z.string().min(10).optional().describe("(Manual path) Firebase refresh token. Prefer firebase_paste.")
6766
7145
  },
6767
7146
  async (args) => {
6768
7147
  const existing = readCredentials();
@@ -6885,8 +7264,8 @@ function registerLeadCaptureTool(server2) {
6885
7264
  "request_license",
6886
7265
  "Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97 one-time \u2014 212 tools across 43 modules, 3-machine activation, no subscription. Leave your email and we'll send the purchase link + setup help; the tool also returns where to buy right now.",
6887
7266
  {
6888
- email: import_zod37.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
6889
- name: import_zod37.z.string().optional().describe("Your name (optional).")
7267
+ email: import_zod38.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
7268
+ name: import_zod38.z.string().optional().describe("Your name (optional).")
6890
7269
  },
6891
7270
  async (args) => {
6892
7271
  const buyUrl = "https://elitedcs.com/ghl-mcp-server";
@@ -7005,7 +7384,7 @@ Token registry: ${registeredCount} location(s) registered${versionLine}`
7005
7384
  "switch_location",
7006
7385
  "Switch the active GHL sub-account. Automatically swaps the API key from the token registry if available. After switching, all tools default to the new location.",
7007
7386
  {
7008
- locationId: import_zod38.z.string().describe("The Location ID to switch to.")
7387
+ locationId: import_zod39.z.string().describe("The Location ID to switch to.")
7009
7388
  },
7010
7389
  async ({ locationId: locationId2 }) => withSwitchLock(async () => {
7011
7390
  const previousId = client.defaultLocationId;
@@ -7078,9 +7457,9 @@ Still on: ${previousId || "none"}${hint}` }],
7078
7457
  "register_location",
7079
7458
  "Add a GHL sub-account to the token registry so switch_location can automatically use its API key. Each sub-account needs its own Private Integration key created in GHL Settings > Integrations.",
7080
7459
  {
7081
- locationId: import_zod38.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
7082
- name: import_zod38.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
7083
- apiKey: import_zod38.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
7460
+ locationId: import_zod39.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
7461
+ name: import_zod39.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
7462
+ apiKey: import_zod39.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
7084
7463
  },
7085
7464
  async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
7086
7465
  if (!registry2) {
@@ -7137,7 +7516,7 @@ The API key could not access location ${locationId2}. Make sure:
7137
7516
  "register_agency_key",
7138
7517
  "Store the AGENCY-level (company-scoped) API key in the token registry. This key powers agency-wide tools: list_snapshots, create_snapshot_share_link, and list_available_locations across all sub-accounts. Create it at the AGENCY level in GHL (Agency Settings > Private Integrations) \u2014 it is different from a sub-account's key. The key is validated before saving.",
7139
7518
  {
7140
- apiKey: import_zod38.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
7519
+ apiKey: import_zod39.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
7141
7520
  },
7142
7521
  async ({ apiKey: apiKey2 }) => {
7143
7522
  if (!registry2) {
@@ -7191,7 +7570,7 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
7191
7570
  "unregister_location",
7192
7571
  "Remove a GHL sub-account from the token registry.",
7193
7572
  {
7194
- locationId: import_zod38.z.string().describe("The Location ID to remove.")
7573
+ locationId: import_zod39.z.string().describe("The Location ID to remove.")
7195
7574
  },
7196
7575
  async ({ locationId: locationId2 }) => {
7197
7576
  if (!registry2) {
@@ -7217,12 +7596,12 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
7217
7596
  "register_company_firebase",
7218
7597
  "Register a GHL company's Firebase credentials so the workflow builder and all Firebase-gated tools work when you switch into THAT company's sub-accounts. Firebase refresh tokens are company-scoped, so managing a client's GHL (e.g. an account where you're an admin user) requires that company's own token. Capture the values from a browser session logged into the client's account. The tool stores them under the company ID the Firebase token itself authenticates as (decoded from the token), so you do NOT need to hunt down the exact internal company ID \u2014 pass whatever ID you have and it self-corrects. After this, switch_location to any of that company's locations authenticates the workflow builder correctly. DevTools capture steps: elitedcs.com/ghl-mcp-firebase.",
7219
7598
  {
7220
- companyId: import_zod38.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
7221
- name: import_zod38.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
7222
- ghl_firebase_refresh_token: import_zod38.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
7223
- ghl_user_id: import_zod38.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
7224
- ghl_firebase_api_key: import_zod38.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
7225
- test_location_id: import_zod38.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
7599
+ companyId: import_zod39.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
7600
+ name: import_zod39.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
7601
+ ghl_firebase_refresh_token: import_zod39.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
7602
+ ghl_user_id: import_zod39.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
7603
+ ghl_firebase_api_key: import_zod39.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
7604
+ test_location_id: import_zod39.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
7226
7605
  },
7227
7606
  async (args) => {
7228
7607
  if (!registry2) {
@@ -7326,7 +7705,7 @@ Now run switch_location to any of this company's sub-accounts \u2014 the workflo
7326
7705
  "unregister_company_firebase",
7327
7706
  "Remove a company's Firebase credentials from the registry. Workflow-builder tools will stop working for that company's sub-accounts until re-registered.",
7328
7707
  {
7329
- companyId: import_zod38.z.string().describe("The company ID to remove Firebase credentials for.")
7708
+ companyId: import_zod39.z.string().describe("The company ID to remove Firebase credentials for.")
7330
7709
  },
7331
7710
  async ({ companyId }) => {
7332
7711
  if (!registry2) {
@@ -7394,8 +7773,8 @@ ${lines.join("\n")}
7394
7773
  "list_available_locations",
7395
7774
  "List all GHL sub-accounts (locations) accessible with the current or agency API key. Shows locations that exist in the GHL account \u2014 use register_location to add their tokens. Offset-based pagination via skip/limit.",
7396
7775
  {
7397
- limit: import_zod38.z.number().optional().describe("Max locations to return. Defaults to 20."),
7398
- skip: import_zod38.z.number().optional().describe("Number to skip for pagination.")
7776
+ limit: import_zod39.z.number().optional().describe("Max locations to return. Defaults to 20."),
7777
+ skip: import_zod39.z.number().optional().describe("Number to skip for pagination.")
7399
7778
  },
7400
7779
  async ({ limit, skip }) => {
7401
7780
  try {
@@ -7438,7 +7817,7 @@ ${lines.join("\n")}
7438
7817
  }
7439
7818
 
7440
7819
  // src/tools/bulk-operations.ts
7441
- var import_zod39 = require("zod");
7820
+ var import_zod40 = require("zod");
7442
7821
  function delay(ms) {
7443
7822
  return new Promise((resolve5) => setTimeout(resolve5, ms));
7444
7823
  }
@@ -7450,8 +7829,8 @@ function registerBulkOperationTools(server2, client) {
7450
7829
  "bulk_add_tags",
7451
7830
  "Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
7452
7831
  {
7453
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
7454
- tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
7832
+ contactIds: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
7833
+ tags: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
7455
7834
  },
7456
7835
  async ({ contactIds, tags }) => {
7457
7836
  const results = { success: 0, failed: 0, errors: [] };
@@ -7473,8 +7852,8 @@ function registerBulkOperationTools(server2, client) {
7473
7852
  "bulk_remove_tags",
7474
7853
  "Remove tags from multiple contacts at once. Rate-limited.",
7475
7854
  {
7476
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
7477
- tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
7855
+ contactIds: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
7856
+ tags: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
7478
7857
  },
7479
7858
  async ({ contactIds, tags }) => {
7480
7859
  const results = { success: 0, failed: 0, errors: [] };
@@ -7495,8 +7874,8 @@ function registerBulkOperationTools(server2, client) {
7495
7874
  "bulk_update_contacts",
7496
7875
  "Update the same field(s) on multiple contacts at once. Rate-limited. Example: set a custom field value, change source, update address for a batch of contacts.",
7497
7876
  {
7498
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
7499
- fields: import_zod39.z.record(import_zod39.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
7877
+ contactIds: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
7878
+ fields: import_zod40.z.record(import_zod40.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
7500
7879
  },
7501
7880
  async ({ contactIds, fields }) => {
7502
7881
  const results = { success: 0, failed: 0, errors: [] };
@@ -7517,8 +7896,8 @@ function registerBulkOperationTools(server2, client) {
7517
7896
  "bulk_add_to_workflow",
7518
7897
  "Enroll multiple contacts into a workflow at once. Rate-limited.",
7519
7898
  {
7520
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
7521
- workflowId: import_zod39.z.string().describe("The workflow ID to enroll contacts into.")
7899
+ contactIds: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
7900
+ workflowId: import_zod40.z.string().describe("The workflow ID to enroll contacts into.")
7522
7901
  },
7523
7902
  async ({ contactIds, workflowId }) => {
7524
7903
  const results = { success: 0, failed: 0, errors: [] };
@@ -7539,8 +7918,8 @@ function registerBulkOperationTools(server2, client) {
7539
7918
  "bulk_delete_contacts",
7540
7919
  "Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
7541
7920
  {
7542
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
7543
- confirm: import_zod39.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
7921
+ contactIds: import_zod40.z.array(import_zod40.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
7922
+ confirm: import_zod40.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
7544
7923
  },
7545
7924
  async ({ contactIds, confirm }) => {
7546
7925
  if (confirm !== "DELETE") {
@@ -7563,7 +7942,7 @@ function registerBulkOperationTools(server2, client) {
7563
7942
  }
7564
7943
 
7565
7944
  // src/tools/account-export.ts
7566
- var import_zod40 = require("zod");
7945
+ var import_zod41 = require("zod");
7567
7946
  function delay2(ms) {
7568
7947
  return new Promise((resolve5) => setTimeout(resolve5, ms));
7569
7948
  }
@@ -7573,8 +7952,8 @@ function registerAccountExportTools(server2, client) {
7573
7952
  "export_account",
7574
7953
  "Export a complete inventory of the GHL sub-account: location info, contacts (count + sample), pipelines with stages, workflows (with full actions if builder auth is configured), funnels with pages, forms, custom fields, custom values, tags, calendars, and users. Returns a comprehensive JSON report for auditing or backup.",
7575
7954
  {
7576
- locationId: import_zod40.z.string().optional().describe("Location ID to export. Uses default if not specified."),
7577
- includeContacts: import_zod40.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
7955
+ locationId: import_zod41.z.string().optional().describe("Location ID to export. Uses default if not specified."),
7956
+ includeContacts: import_zod41.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
7578
7957
  },
7579
7958
  async ({ locationId: locationId2, includeContacts }) => {
7580
7959
  try {
@@ -7702,8 +8081,8 @@ function registerAccountExportTools(server2, client) {
7702
8081
  "compare_locations",
7703
8082
  "Compare two GHL sub-accounts side by side \u2014 shows differences in pipelines, workflows, custom fields, tags, forms, and funnels. Useful for ensuring consistency across locations or auditing before/after changes.",
7704
8083
  {
7705
- locationA: import_zod40.z.string().describe("First Location ID."),
7706
- locationB: import_zod40.z.string().describe("Second Location ID.")
8084
+ locationA: import_zod41.z.string().describe("First Location ID."),
8085
+ locationB: import_zod41.z.string().describe("Second Location ID.")
7707
8086
  },
7708
8087
  async ({ locationA, locationB }) => {
7709
8088
  try {
@@ -7781,7 +8160,7 @@ function registerAccountExportTools(server2, client) {
7781
8160
  }
7782
8161
 
7783
8162
  // src/tools/workflow-cloner.ts
7784
- var import_zod41 = require("zod");
8163
+ var import_zod42 = require("zod");
7785
8164
  var crypto3 = __toESM(require("crypto"));
7786
8165
  function registerWorkflowClonerTools(server2, builderClient) {
7787
8166
  const client = builderClient;
@@ -7790,8 +8169,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
7790
8169
  "clone_workflow",
7791
8170
  "Deep clone a workflow \u2014 creates an exact copy with new IDs for all actions, triggers, and references. The clone starts as a draft. Useful for creating templates or duplicating workflows across projects.",
7792
8171
  {
7793
- sourceWorkflowId: import_zod41.z.string().describe("The workflow ID to clone."),
7794
- newName: import_zod41.z.string().describe("Name for the cloned workflow.")
8172
+ sourceWorkflowId: import_zod42.z.string().describe("The workflow ID to clone."),
8173
+ newName: import_zod42.z.string().describe("Name for the cloned workflow.")
7795
8174
  },
7796
8175
  async ({ sourceWorkflowId, newName }) => {
7797
8176
  try {
@@ -7880,7 +8259,7 @@ function registerWorkflowClonerTools(server2, builderClient) {
7880
8259
  }
7881
8260
 
7882
8261
  // src/tools/smart-lists.ts
7883
- var import_zod42 = require("zod");
8262
+ var import_zod43 = require("zod");
7884
8263
  var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
7885
8264
  var OBJECT_KEYS = ["contacts", "opportunity"];
7886
8265
  function registerSmartListTools(server2, builderClient) {
@@ -7907,11 +8286,11 @@ ${text2}`);
7907
8286
  "list_smart_lists",
7908
8287
  "List smart lists (dynamic / saved-filter lists) in a location. Smart Lists are saved searches over contacts or opportunities \u2014 agencies use them to segment by complex criteria. Filters and columns aren't returned in the list view; use get_smart_list for the full filter spec.",
7909
8288
  {
7910
- objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
7911
- query: import_zod42.z.string().optional().describe("Free-text search across smart list names."),
7912
- limit: import_zod42.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
7913
- startAfter: import_zod42.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
7914
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8289
+ objectKey: import_zod43.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
8290
+ query: import_zod43.z.string().optional().describe("Free-text search across smart list names."),
8291
+ limit: import_zod43.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
8292
+ startAfter: import_zod43.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
8293
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7915
8294
  },
7916
8295
  async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
7917
8296
  try {
@@ -7931,8 +8310,8 @@ ${text2}`);
7931
8310
  "get_smart_list",
7932
8311
  "Get a single smart list by ID with its full configuration: filters, columns, permissions, and metadata. The filters array is what defines who/what is in the list.",
7933
8312
  {
7934
- listId: import_zod42.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
7935
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8313
+ listId: import_zod43.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
8314
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7936
8315
  },
7937
8316
  async ({ listId, locationId: locationId2 }) => {
7938
8317
  try {
@@ -7948,13 +8327,13 @@ ${text2}`);
7948
8327
  "create_smart_list",
7949
8328
  "Create a new smart list (dynamic filter list). Required: name + objectKey. Filters define the saved-search criteria \u2014 pass an empty array to create an empty list and add filters later via update_smart_list. The shape of filters/columns is opaque here; query an existing smart list with get_smart_list to see the format GHL expects.",
7950
8329
  {
7951
- name: import_zod42.z.string().describe("Display name for the smart list."),
7952
- objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
7953
- filters: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
7954
- columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
7955
- pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
7956
- defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
7957
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8330
+ name: import_zod43.z.string().describe("Display name for the smart list."),
8331
+ objectKey: import_zod43.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
8332
+ filters: import_zod43.z.array(import_zod43.z.record(import_zod43.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
8333
+ columns: import_zod43.z.array(import_zod43.z.record(import_zod43.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
8334
+ pipelineIds: import_zod43.z.array(import_zod43.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
8335
+ defaultInPipelines: import_zod43.z.array(import_zod43.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
8336
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7958
8337
  },
7959
8338
  async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
7960
8339
  try {
@@ -7975,13 +8354,13 @@ ${text2}`);
7975
8354
  "update_smart_list",
7976
8355
  "Update an existing smart list's name, filters, or columns. The objectKey CANNOT be changed after creation (GHL rejects with 422 if you try). Use get_smart_list first to inspect the current filters; partial updates work \u2014 pass only the fields you want to change.",
7977
8356
  {
7978
- listId: import_zod42.z.string().describe("The smart list ID to update."),
7979
- name: import_zod42.z.string().optional().describe("New display name."),
7980
- filters: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
7981
- columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the column array entirely."),
7982
- pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
7983
- defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
7984
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8357
+ listId: import_zod43.z.string().describe("The smart list ID to update."),
8358
+ name: import_zod43.z.string().optional().describe("New display name."),
8359
+ filters: import_zod43.z.array(import_zod43.z.record(import_zod43.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
8360
+ columns: import_zod43.z.array(import_zod43.z.record(import_zod43.z.unknown())).optional().describe("Replace the column array entirely."),
8361
+ pipelineIds: import_zod43.z.array(import_zod43.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
8362
+ defaultInPipelines: import_zod43.z.array(import_zod43.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
8363
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7985
8364
  },
7986
8365
  async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
7987
8366
  try {
@@ -8006,9 +8385,9 @@ ${text2}`);
8006
8385
  "delete_smart_list",
8007
8386
  "Permanently delete a smart list. IRREVERSIBLE. The list configuration is removed but the contacts/opportunities themselves are NOT touched \u2014 smart lists are just saved filters. Any workflow trigger / dashboard / report that referenced this list by ID will stop working.",
8008
8387
  {
8009
- listId: import_zod42.z.string().describe("The smart list ID to delete."),
8010
- confirm: import_zod42.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
8011
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8388
+ listId: import_zod43.z.string().describe("The smart list ID to delete."),
8389
+ confirm: import_zod43.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
8390
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8012
8391
  },
8013
8392
  async ({ listId, locationId: locationId2 }) => {
8014
8393
  try {
@@ -8023,7 +8402,7 @@ ${text2}`);
8023
8402
  }
8024
8403
 
8025
8404
  // src/tools/reputation.ts
8026
- var import_zod43 = require("zod");
8405
+ var import_zod44 = require("zod");
8027
8406
  var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
8028
8407
  function registerReputationTools(server2, builderClient) {
8029
8408
  const client = builderClient;
@@ -8045,7 +8424,7 @@ ${text2}`);
8045
8424
  "get_review_link_list",
8046
8425
  "List the review-link destinations configured for a location \u2014 the platforms (Google, Facebook, etc.) where review requests send contacts. Each entry has a label and the public review URL. Useful for: building review-request workflows (the workflow goal condition `review_request_clicked` references these review-link ids), and auditing which review platforms a sub-account has connected.",
8047
8426
  {
8048
- locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8427
+ locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8049
8428
  },
8050
8429
  async ({ locationId: locationId2 }) => {
8051
8430
  const loc = locationId2 ?? client.locationId;
@@ -8057,11 +8436,11 @@ ${text2}`);
8057
8436
  "list_reviews",
8058
8437
  "List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, and source. Supports paging and an optional rating filter. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
8059
8438
  {
8060
- locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
8061
- pageNumber: import_zod43.z.number().optional().describe("1-based page number. Defaults to 1."),
8062
- pageSize: import_zod43.z.number().optional().describe("Results per page. Defaults to 10."),
8063
- rating: import_zod43.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
8064
- includeDeleted: import_zod43.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
8439
+ locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
8440
+ pageNumber: import_zod44.z.number().optional().describe("1-based page number. Defaults to 1."),
8441
+ pageSize: import_zod44.z.number().optional().describe("Results per page. Defaults to 10."),
8442
+ rating: import_zod44.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
8443
+ includeDeleted: import_zod44.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
8065
8444
  },
8066
8445
  async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
8067
8446
  const loc = locationId2 ?? client.locationId;
@@ -8091,7 +8470,7 @@ function buildReviewsQuery(locationId2, opts = {}) {
8091
8470
  }
8092
8471
 
8093
8472
  // src/tools/email-campaigns.ts
8094
- var import_zod44 = require("zod");
8473
+ var import_zod45 = require("zod");
8095
8474
  var SVC_BASE = "https://services.leadconnectorhq.com";
8096
8475
  function registerEmailCampaignTools(server2, builderClient) {
8097
8476
  const client = builderClient;
@@ -8101,15 +8480,15 @@ function registerEmailCampaignTools(server2, builderClient) {
8101
8480
  "create_email_campaign",
8102
8481
  "Create an email campaign / broadcast DRAFT from an existing email template. Requires a templateId (create one first with create_email_template). The campaign is created as a draft \u2014 to actually SEND or schedule it, finish in the GHL UI: the send/schedule endpoint isn't available through the API yet. There's also no API delete for campaigns, so drafts created here are removed via the GHL UI. Despite those limits, this gets the campaign 90% built \u2014 template, subject, sender, name all set programmatically.",
8103
8482
  {
8104
- templateId: import_zod44.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
8105
- name: import_zod44.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
8106
- subject: import_zod44.z.string().optional().describe("Email subject line recipients see."),
8107
- fromName: import_zod44.z.string().optional().describe("Sender display name."),
8108
- fromEmail: import_zod44.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
8109
- isPlainText: import_zod44.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
8110
- enableResendToUnopened: import_zod44.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
8111
- hasUtmTracking: import_zod44.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
8112
- locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8483
+ templateId: import_zod45.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
8484
+ name: import_zod45.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
8485
+ subject: import_zod45.z.string().optional().describe("Email subject line recipients see."),
8486
+ fromName: import_zod45.z.string().optional().describe("Sender display name."),
8487
+ fromEmail: import_zod45.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
8488
+ isPlainText: import_zod45.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
8489
+ enableResendToUnopened: import_zod45.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
8490
+ hasUtmTracking: import_zod45.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
8491
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8113
8492
  },
8114
8493
  async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
8115
8494
  const loc = locationId2 ?? client.locationId;
@@ -8143,7 +8522,7 @@ ${text2}`);
8143
8522
  }
8144
8523
 
8145
8524
  // src/tools/memberships.ts
8146
- var import_zod45 = require("zod");
8525
+ var import_zod46 = require("zod");
8147
8526
  var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
8148
8527
  function registerMembershipTools(server2, builderClient) {
8149
8528
  const client = builderClient;
@@ -8169,7 +8548,7 @@ ${text2}`);
8169
8548
  "list_membership_offers",
8170
8549
  "List a location's membership offers and products in one call. Returns { products: [...], offers: [...] }. Products are courses/communities; offers are the access grants (what a contact gets enrolled in). Use the returned ids with membership trigger conditions like offer_access_granted / product_completed.",
8171
8550
  {
8172
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8551
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8173
8552
  },
8174
8553
  async ({ locationId: locationId2 }) => {
8175
8554
  const loc = locationId2 ?? client.locationId;
@@ -8181,8 +8560,8 @@ ${text2}`);
8181
8560
  "list_membership_categories",
8182
8561
  "List all membership/course categories in a location. Categories group lessons inside a course/product. Use the returned ids with the category_completed / category_started trigger conditions. READ-ONLY.",
8183
8562
  {
8184
- limit: import_zod45.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
8185
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8563
+ limit: import_zod46.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
8564
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8186
8565
  },
8187
8566
  async ({ limit, locationId: locationId2 }) => {
8188
8567
  const loc = locationId2 ?? client.locationId;
@@ -8194,8 +8573,8 @@ ${text2}`);
8194
8573
  "list_membership_lessons",
8195
8574
  "List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
8196
8575
  {
8197
- limit: import_zod45.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
8198
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8576
+ limit: import_zod46.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
8577
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8199
8578
  },
8200
8579
  async ({ limit, locationId: locationId2 }) => {
8201
8580
  const loc = locationId2 ?? client.locationId;
@@ -8207,9 +8586,9 @@ ${text2}`);
8207
8586
  "create_course",
8208
8587
  "Create a membership course (a 'product') in a location. Creates the course shell with a title and description; add categories (create_membership_category) and lessons (create_membership_lesson) into it, and an offer (create_membership_offer) to grant access. Returns the new product, including its id. Requires Firebase auth.",
8209
8588
  {
8210
- title: import_zod45.z.string().describe("Course title."),
8211
- description: import_zod45.z.string().optional().describe("Course description. Defaults to empty."),
8212
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8589
+ title: import_zod46.z.string().describe("Course title."),
8590
+ description: import_zod46.z.string().optional().describe("Course description. Defaults to empty."),
8591
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8213
8592
  },
8214
8593
  async ({ title, description, locationId: locationId2 }) => {
8215
8594
  const loc = locationId2 ?? client.locationId;
@@ -8221,13 +8600,13 @@ ${text2}`);
8221
8600
  "create_membership_category",
8222
8601
  "Create a category (module/section) inside a membership course. Categories group lessons. Needs the productId of the course (from create_course or list_membership_offers). Returns the new category, including its id (use it as categoryId when creating lessons). Requires Firebase auth.",
8223
8602
  {
8224
- title: import_zod45.z.string().describe("Category title (e.g. 'Module 1')."),
8225
- productId: import_zod45.z.string().describe("The course/product id this category belongs to."),
8226
- description: import_zod45.z.string().optional().describe("Category description. Defaults to empty."),
8227
- visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8228
- sequenceNo: import_zod45.z.number().optional().describe("Display order within the course. Defaults to 0."),
8229
- dripDays: import_zod45.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
8230
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8603
+ title: import_zod46.z.string().describe("Category title (e.g. 'Module 1')."),
8604
+ productId: import_zod46.z.string().describe("The course/product id this category belongs to."),
8605
+ description: import_zod46.z.string().optional().describe("Category description. Defaults to empty."),
8606
+ visibility: import_zod46.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8607
+ sequenceNo: import_zod46.z.number().optional().describe("Display order within the course. Defaults to 0."),
8608
+ dripDays: import_zod46.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
8609
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8231
8610
  },
8232
8611
  async ({ title, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
8233
8612
  const loc = locationId2 ?? client.locationId;
@@ -8239,14 +8618,14 @@ ${text2}`);
8239
8618
  "create_membership_lesson",
8240
8619
  "Create a lesson (a 'post') inside a membership course category. Needs both the categoryId (from create_membership_category) and the productId of the course. Description is the lesson body as HTML. Returns the new lesson, including its id. Requires Firebase auth.",
8241
8620
  {
8242
- title: import_zod45.z.string().describe("Lesson title."),
8243
- categoryId: import_zod45.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
8244
- productId: import_zod45.z.string().describe("The course/product id this lesson belongs to."),
8245
- description: import_zod45.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
8246
- contentType: import_zod45.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
8247
- visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8248
- sequenceNo: import_zod45.z.number().optional().describe("Display order within the category. Defaults to 0."),
8249
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8621
+ title: import_zod46.z.string().describe("Lesson title."),
8622
+ categoryId: import_zod46.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
8623
+ productId: import_zod46.z.string().describe("The course/product id this lesson belongs to."),
8624
+ description: import_zod46.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
8625
+ contentType: import_zod46.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
8626
+ visibility: import_zod46.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8627
+ sequenceNo: import_zod46.z.number().optional().describe("Display order within the category. Defaults to 0."),
8628
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8250
8629
  },
8251
8630
  async ({ title, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
8252
8631
  const loc = locationId2 ?? client.locationId;
@@ -8258,12 +8637,12 @@ ${text2}`);
8258
8637
  "create_membership_offer",
8259
8638
  "Create a membership offer \u2014 the access grant that enrolls contacts into one or more courses/products. Link it to course product ids. Defaults to a free offer; for paid, set type to 'recurring' or 'one_time' with an amount. Returns the new offer, including its id (referenced by the offer_access_granted trigger). Requires Firebase auth.",
8260
8639
  {
8261
- title: import_zod45.z.string().describe("Offer title (shown at checkout / in the offer list)."),
8262
- productIds: import_zod45.z.array(import_zod45.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
8263
- type: import_zod45.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
8264
- amount: import_zod45.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
8265
- currency: import_zod45.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
8266
- locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8640
+ title: import_zod46.z.string().describe("Offer title (shown at checkout / in the offer list)."),
8641
+ productIds: import_zod46.z.array(import_zod46.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
8642
+ type: import_zod46.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
8643
+ amount: import_zod46.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
8644
+ currency: import_zod46.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
8645
+ locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
8267
8646
  },
8268
8647
  async ({ title, productIds, type, amount, currency, locationId: locationId2 }) => {
8269
8648
  const loc = locationId2 ?? client.locationId;
@@ -8322,41 +8701,41 @@ function buildOfferPayload(o) {
8322
8701
  }
8323
8702
 
8324
8703
  // src/tools/template-deployer.ts
8325
- var import_zod46 = require("zod");
8704
+ var import_zod47 = require("zod");
8326
8705
  var fs4 = __toESM(require("fs"));
8327
8706
  var path4 = __toESM(require("path"));
8328
8707
  function delay3(ms) {
8329
8708
  return new Promise((resolve5) => setTimeout(resolve5, ms));
8330
8709
  }
8331
- var TemplateSchema = import_zod46.z.object({
8332
- templateName: import_zod46.z.string(),
8333
- templateVersion: import_zod46.z.string().optional(),
8334
- description: import_zod46.z.string().optional().default(""),
8335
- questionnaire: import_zod46.z.array(import_zod46.z.object({
8336
- id: import_zod46.z.string(),
8337
- question: import_zod46.z.string(),
8338
- type: import_zod46.z.string(),
8339
- required: import_zod46.z.boolean().optional(),
8340
- placeholder: import_zod46.z.string().optional()
8710
+ var TemplateSchema = import_zod47.z.object({
8711
+ templateName: import_zod47.z.string(),
8712
+ templateVersion: import_zod47.z.string().optional(),
8713
+ description: import_zod47.z.string().optional().default(""),
8714
+ questionnaire: import_zod47.z.array(import_zod47.z.object({
8715
+ id: import_zod47.z.string(),
8716
+ question: import_zod47.z.string(),
8717
+ type: import_zod47.z.string(),
8718
+ required: import_zod47.z.boolean().optional(),
8719
+ placeholder: import_zod47.z.string().optional()
8341
8720
  })).optional().default([]),
8342
- location: import_zod46.z.record(import_zod46.z.unknown()).optional(),
8343
- tags: import_zod46.z.array(import_zod46.z.string()).optional(),
8344
- customFields: import_zod46.z.array(import_zod46.z.object({
8345
- name: import_zod46.z.string(),
8346
- dataType: import_zod46.z.string()
8721
+ location: import_zod47.z.record(import_zod47.z.unknown()).optional(),
8722
+ tags: import_zod47.z.array(import_zod47.z.string()).optional(),
8723
+ customFields: import_zod47.z.array(import_zod47.z.object({
8724
+ name: import_zod47.z.string(),
8725
+ dataType: import_zod47.z.string()
8347
8726
  })).optional(),
8348
- pipelines: import_zod46.z.array(import_zod46.z.object({
8349
- name: import_zod46.z.string(),
8350
- stages: import_zod46.z.array(import_zod46.z.object({ position: import_zod46.z.number(), name: import_zod46.z.string() }))
8727
+ pipelines: import_zod47.z.array(import_zod47.z.object({
8728
+ name: import_zod47.z.string(),
8729
+ stages: import_zod47.z.array(import_zod47.z.object({ position: import_zod47.z.number(), name: import_zod47.z.string() }))
8351
8730
  })).optional(),
8352
- workflows: import_zod46.z.array(import_zod46.z.object({
8353
- name: import_zod46.z.string(),
8354
- condition: import_zod46.z.string().optional(),
8355
- actions: import_zod46.z.array(import_zod46.z.record(import_zod46.z.unknown())).optional().default([])
8731
+ workflows: import_zod47.z.array(import_zod47.z.object({
8732
+ name: import_zod47.z.string(),
8733
+ condition: import_zod47.z.string().optional(),
8734
+ actions: import_zod47.z.array(import_zod47.z.record(import_zod47.z.unknown())).optional().default([])
8356
8735
  })).optional(),
8357
- calendars: import_zod46.z.array(import_zod46.z.object({
8358
- name: import_zod46.z.string(),
8359
- description: import_zod46.z.string().optional()
8736
+ calendars: import_zod47.z.array(import_zod47.z.object({
8737
+ name: import_zod47.z.string(),
8738
+ description: import_zod47.z.string().optional()
8360
8739
  })).optional()
8361
8740
  });
8362
8741
  function registerTemplateDeployerTools(server2, client) {
@@ -8427,7 +8806,7 @@ function registerTemplateDeployerTools(server2, client) {
8427
8806
  "get_template_questionnaire",
8428
8807
  "Get the questionnaire for a specific template. Returns all the questions that need to be answered before deploying. Present these to the user one at a time in a conversational style.",
8429
8808
  {
8430
- templateFile: import_zod46.z.string().describe("Path to the template JSON file (from list_templates).")
8809
+ templateFile: import_zod47.z.string().describe("Path to the template JSON file (from list_templates).")
8431
8810
  },
8432
8811
  async ({ templateFile }) => {
8433
8812
  try {
@@ -8460,10 +8839,10 @@ function registerTemplateDeployerTools(server2, client) {
8460
8839
  "deploy_template",
8461
8840
  "Deploy a template to set up a GHL sub-account. Creates tags, custom fields, pipelines with stages, calendars, workflows, and forms based on the template and the user's questionnaire answers. This is the main setup automation tool.",
8462
8841
  {
8463
- templateFile: import_zod46.z.string().describe("Path to the template JSON file."),
8464
- answers: import_zod46.z.record(import_zod46.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
8465
- locationId: import_zod46.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
8466
- dryRun: import_zod46.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
8842
+ templateFile: import_zod47.z.string().describe("Path to the template JSON file."),
8843
+ answers: import_zod47.z.record(import_zod47.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
8844
+ locationId: import_zod47.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
8845
+ dryRun: import_zod47.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
8467
8846
  },
8468
8847
  async ({ templateFile, answers, locationId: locationId2, dryRun }) => {
8469
8848
  try {
@@ -8709,7 +9088,7 @@ ${errors.join("\n")}` : "\nNo errors!",
8709
9088
  }
8710
9089
 
8711
9090
  // src/tools/validators.ts
8712
- var import_zod47 = require("zod");
9091
+ var import_zod48 = require("zod");
8713
9092
  var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
8714
9093
  var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
8715
9094
  "first_name",
@@ -9167,7 +9546,7 @@ function registerValidatorTools(server2, client, builderClient) {
9167
9546
  server2.tool(
9168
9547
  "validate_workflow",
9169
9548
  "Pre-flight ID validation for ONE deployed GHL workflow. Scans every trigger and action for references to pipelines, pipeline stages, custom fields, users, workflows, forms, calendars, and surveys; verifies each ID exists in the current location. Use BEFORE publish_workflow when a workflow was edited, or when a published workflow stops behaving. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions. Never reports a false break \u2014 anything it cannot fully verify is marked 'unverified', not 'error'.",
9170
- { workflowId: import_zod47.z.string().describe("The workflow ID to validate.") },
9549
+ { workflowId: import_zod48.z.string().describe("The workflow ID to validate.") },
9171
9550
  async ({ workflowId }) => {
9172
9551
  try {
9173
9552
  const workflow = await builderClient.getWorkflow(workflowId);
@@ -9419,10 +9798,10 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
9419
9798
  }
9420
9799
 
9421
9800
  // src/tools/snapshots.ts
9422
- var import_zod48 = require("zod");
9423
- var SnapshotSchema = import_zod48.z.object({ id: import_zod48.z.string(), name: import_zod48.z.string(), type: import_zod48.z.string() }).passthrough();
9424
- var SnapshotsResponseSchema = import_zod48.z.object({ snapshots: import_zod48.z.array(SnapshotSchema) }).passthrough();
9425
- var ShareLinkResponseSchema = import_zod48.z.object({ id: import_zod48.z.string(), shareLink: import_zod48.z.string() }).passthrough();
9801
+ var import_zod49 = require("zod");
9802
+ var SnapshotSchema = import_zod49.z.object({ id: import_zod49.z.string(), name: import_zod49.z.string(), type: import_zod49.z.string() }).passthrough();
9803
+ var SnapshotsResponseSchema = import_zod49.z.object({ snapshots: import_zod49.z.array(SnapshotSchema) }).passthrough();
9804
+ var ShareLinkResponseSchema = import_zod49.z.object({ id: import_zod49.z.string(), shareLink: import_zod49.z.string() }).passthrough();
9426
9805
  var SHARE_TYPES = [
9427
9806
  "link",
9428
9807
  "permanent_link",
@@ -9469,7 +9848,7 @@ function registerSnapshotTools(server2, client, registry2) {
9469
9848
  "list_snapshots",
9470
9849
  "List the agency's GHL snapshots (id, name, type) so you can pick the right one by name before applying it to a sub-account. Reads the agency/company-scoped key (not a sub-account PIT). companyId defaults to the active location's company; pass it explicitly to target a different company you have the agency key for. Read-only.",
9471
9850
  {
9472
- companyId: import_zod48.z.string().optional().describe(
9851
+ companyId: import_zod49.z.string().optional().describe(
9473
9852
  "Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
9474
9853
  )
9475
9854
  },
@@ -9494,11 +9873,11 @@ function registerSnapshotTools(server2, client, registry2) {
9494
9873
  "create_snapshot_share_link",
9495
9874
  "Create a shareable load link for one of the agency's snapshots (returns a gohighlevel.com/?share=... URL to import the snapshot into a sub-account). Uses the agency/company-scoped key. WARNING: this is NOT idempotent \u2014 each call mints a NEW link, and there is no API to list or revoke links (revoke in the GHL UI under the snapshot's share settings). Pick share_type deliberately. Use list_snapshots first to get the snapshot id.",
9496
9875
  {
9497
- snapshot_id: import_zod48.z.string().describe("The snapshot id to share (from list_snapshots)."),
9498
- share_type: import_zod48.z.enum(SHARE_TYPES).describe(
9876
+ snapshot_id: import_zod49.z.string().describe("The snapshot id to share (from list_snapshots)."),
9877
+ share_type: import_zod49.z.enum(SHARE_TYPES).describe(
9499
9878
  "Share link type. 'link' = standard share link; 'permanent_link' = non-expiring; 'agency_link' = share to agencies; 'location_link' = load into a sub-account/location; 'marketplace_link' = marketplace listing. No default \u2014 choose intentionally."
9500
9879
  ),
9501
- companyId: import_zod48.z.string().optional().describe(
9880
+ companyId: import_zod49.z.string().optional().describe(
9502
9881
  "Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
9503
9882
  )
9504
9883
  },
@@ -9528,17 +9907,17 @@ function registerSnapshotTools(server2, client, registry2) {
9528
9907
  }
9529
9908
 
9530
9909
  // src/tools/phone.ts
9531
- var import_zod49 = require("zod");
9532
- var PhoneNumberSchema = import_zod49.z.object({ sid: import_zod49.z.string(), value: import_zod49.z.string(), title: import_zod49.z.string().optional() }).passthrough();
9533
- var NumbersResponseSchema = import_zod49.z.object({ phoneNumbers: import_zod49.z.array(PhoneNumberSchema) }).passthrough();
9534
- var PoolsResponseSchema = import_zod49.z.object({ pools: import_zod49.z.array(import_zod49.z.object({}).passthrough()) }).passthrough();
9910
+ var import_zod50 = require("zod");
9911
+ var PhoneNumberSchema = import_zod50.z.object({ sid: import_zod50.z.string(), value: import_zod50.z.string(), title: import_zod50.z.string().optional() }).passthrough();
9912
+ var NumbersResponseSchema = import_zod50.z.object({ phoneNumbers: import_zod50.z.array(PhoneNumberSchema) }).passthrough();
9913
+ var PoolsResponseSchema = import_zod50.z.object({ pools: import_zod50.z.array(import_zod50.z.object({}).passthrough()) }).passthrough();
9535
9914
  function registerPhoneTools(server2, client) {
9536
9915
  safeTool(
9537
9916
  server2,
9538
9917
  "list_phone_numbers",
9539
9918
  "List the LC Phone numbers provisioned for a location (sid, number, label). Read-only. Use to verify or count purchased numbers (e.g. provisioning step 11). Number purchase is not exposed (billable write).",
9540
9919
  {
9541
- locationId: import_zod49.z.string().optional().describe("Defaults to the active location.")
9920
+ locationId: import_zod50.z.string().optional().describe("Defaults to the active location.")
9542
9921
  },
9543
9922
  async ({ locationId: locationId2 }) => {
9544
9923
  const loc = client.resolveLocationId(locationId2);
@@ -9556,7 +9935,7 @@ function registerPhoneTools(server2, client) {
9556
9935
  "list_number_pools",
9557
9936
  "List LC Phone number pools configured for a location. Read-only.",
9558
9937
  {
9559
- locationId: import_zod49.z.string().optional().describe("Defaults to the active location.")
9938
+ locationId: import_zod50.z.string().optional().describe("Defaults to the active location.")
9560
9939
  },
9561
9940
  async ({ locationId: locationId2 }) => {
9562
9941
  const loc = client.resolveLocationId(locationId2);
@@ -9568,10 +9947,10 @@ function registerPhoneTools(server2, client) {
9568
9947
  }
9569
9948
 
9570
9949
  // src/tools/account-health.ts
9571
- var import_zod50 = require("zod");
9572
- var MetaTotalSchema = import_zod50.z.object({ meta: import_zod50.z.object({ total: import_zod50.z.number() }).passthrough() }).passthrough();
9573
- var TotalSchema = import_zod50.z.object({ total: import_zod50.z.number() }).passthrough();
9574
- var NumbersSchema = import_zod50.z.object({ phoneNumbers: import_zod50.z.array(import_zod50.z.unknown()) }).passthrough();
9950
+ var import_zod51 = require("zod");
9951
+ var MetaTotalSchema = import_zod51.z.object({ meta: import_zod51.z.object({ total: import_zod51.z.number() }).passthrough() }).passthrough();
9952
+ var TotalSchema = import_zod51.z.object({ total: import_zod51.z.number() }).passthrough();
9953
+ var NumbersSchema = import_zod51.z.object({ phoneNumbers: import_zod51.z.array(import_zod51.z.unknown()) }).passthrough();
9575
9954
  var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
9576
9955
  async function section(scope, fn) {
9577
9956
  try {
@@ -9586,8 +9965,8 @@ function registerAccountHealthTools(server2, client) {
9586
9965
  "get_account_health_summary",
9587
9966
  "Account-health summary for a location, composed from existing reads (GHL has no reporting API). Returns: total contacts + NEW contacts in the window; total opportunities + counts by status (open/won/lost/abandoned); total conversations; phone-number count. Every metric is explicitly labeled all_time vs window (with start/end) \u2014 windowed and all-time numbers are never conflated. Sections that can't be read return status:'unavailable' (never a misleading 0). Revenue and appointments are intentionally excluded (not reachable / too costly via the public API).",
9588
9967
  {
9589
- locationId: import_zod50.z.string().optional().describe("Defaults to the active location."),
9590
- windowDays: import_zod50.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
9968
+ locationId: import_zod51.z.string().optional().describe("Defaults to the active location."),
9969
+ windowDays: import_zod51.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
9591
9970
  },
9592
9971
  async ({ locationId: locationId2, windowDays }) => {
9593
9972
  const loc = client.resolveLocationId(locationId2);
@@ -9659,7 +10038,7 @@ function registerAccountHealthTools(server2, client) {
9659
10038
  }
9660
10039
 
9661
10040
  // src/tools/intake-to-build.ts
9662
- var import_zod53 = require("zod");
10041
+ var import_zod54 = require("zod");
9663
10042
 
9664
10043
  // src/intake-to-build/question-set.ts
9665
10044
  var QUESTION_SET_VERSION = "0.1";
@@ -10361,7 +10740,7 @@ function buildPlanFormData(fields, locationId2) {
10361
10740
  }
10362
10741
 
10363
10742
  // src/intake-to-build/brief.ts
10364
- var import_zod51 = require("zod");
10743
+ var import_zod52 = require("zod");
10365
10744
  var BRIEF_SCHEMA_VERSION = "0.1";
10366
10745
  var PRESETS = [
10367
10746
  "generic",
@@ -10371,67 +10750,67 @@ var PRESETS = [
10371
10750
  "ecom",
10372
10751
  "agency"
10373
10752
  ];
10374
- var presetSchema = import_zod51.z.enum(PRESETS);
10753
+ var presetSchema = import_zod52.z.enum(PRESETS);
10375
10754
  var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
10376
- var briefSourceSchema = import_zod51.z.enum(BRIEF_SOURCES);
10377
- var pricePointSchema = import_zod51.z.object({
10378
- name: import_zod51.z.string(),
10379
- price: import_zod51.z.string()
10755
+ var briefSourceSchema = import_zod52.z.enum(BRIEF_SOURCES);
10756
+ var pricePointSchema = import_zod52.z.object({
10757
+ name: import_zod52.z.string(),
10758
+ price: import_zod52.z.string()
10380
10759
  });
10381
- var briefSchema = import_zod51.z.object({
10382
- schemaVersion: import_zod51.z.string(),
10383
- briefId: import_zod51.z.string(),
10760
+ var briefSchema = import_zod52.z.object({
10761
+ schemaVersion: import_zod52.z.string(),
10762
+ briefId: import_zod52.z.string(),
10384
10763
  preset: presetSchema,
10385
10764
  briefSource: briefSourceSchema,
10386
10765
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
10387
- extended: import_zod51.z.record(import_zod51.z.unknown()).optional(),
10388
- business: import_zod51.z.object({
10389
- name: import_zod51.z.string(),
10390
- type: import_zod51.z.string().optional(),
10391
- website: import_zod51.z.string().optional(),
10392
- location: import_zod51.z.string().optional(),
10393
- timezone: import_zod51.z.string().optional(),
10766
+ extended: import_zod52.z.record(import_zod52.z.unknown()).optional(),
10767
+ business: import_zod52.z.object({
10768
+ name: import_zod52.z.string(),
10769
+ type: import_zod52.z.string().optional(),
10770
+ website: import_zod52.z.string().optional(),
10771
+ location: import_zod52.z.string().optional(),
10772
+ timezone: import_zod52.z.string().optional(),
10394
10773
  // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
10395
10774
  // the same tolerance reason as business.type (don't reject valid briefs).
10396
- teamSize: import_zod51.z.string().optional(),
10397
- monthlyLeadVolume: import_zod51.z.string().optional(),
10398
- hours: import_zod51.z.string().optional()
10775
+ teamSize: import_zod52.z.string().optional(),
10776
+ monthlyLeadVolume: import_zod52.z.string().optional(),
10777
+ hours: import_zod52.z.string().optional()
10399
10778
  }).passthrough(),
10400
- offer: import_zod51.z.object({
10401
- summary: import_zod51.z.string().optional(),
10779
+ offer: import_zod52.z.object({
10780
+ summary: import_zod52.z.string().optional(),
10402
10781
  // Parsed best-effort; tolerate a raw string when parsing was not possible.
10403
- pricePoints: import_zod51.z.union([import_zod51.z.array(pricePointSchema), import_zod51.z.string()]).optional(),
10404
- leadMagnet: import_zod51.z.string().optional(),
10405
- avgDealValue: import_zod51.z.string().optional()
10782
+ pricePoints: import_zod52.z.union([import_zod52.z.array(pricePointSchema), import_zod52.z.string()]).optional(),
10783
+ leadMagnet: import_zod52.z.string().optional(),
10784
+ avgDealValue: import_zod52.z.string().optional()
10406
10785
  }).passthrough().optional(),
10407
- audience: import_zod51.z.object({
10408
- ideal: import_zod51.z.string().optional(),
10409
- painPoints: import_zod51.z.array(import_zod51.z.string()).optional(),
10410
- objections: import_zod51.z.array(import_zod51.z.string()).optional()
10786
+ audience: import_zod52.z.object({
10787
+ ideal: import_zod52.z.string().optional(),
10788
+ painPoints: import_zod52.z.array(import_zod52.z.string()).optional(),
10789
+ objections: import_zod52.z.array(import_zod52.z.string()).optional()
10411
10790
  }).passthrough().optional(),
10412
- goal: import_zod51.z.object({
10791
+ goal: import_zod52.z.object({
10413
10792
  // Kept as string: the form option labels are the canonical values, but
10414
10793
  // the contract sample shortens some (e.g. "high-touch"). See §7 note.
10415
- primary: import_zod51.z.string(),
10416
- salesStages: import_zod51.z.array(import_zod51.z.string()).optional(),
10417
- bookingNeeded: import_zod51.z.boolean().optional(),
10418
- followUpStyle: import_zod51.z.string().optional()
10794
+ primary: import_zod52.z.string(),
10795
+ salesStages: import_zod52.z.array(import_zod52.z.string()).optional(),
10796
+ bookingNeeded: import_zod52.z.boolean().optional(),
10797
+ followUpStyle: import_zod52.z.string().optional()
10419
10798
  }).passthrough(),
10420
- channels: import_zod51.z.object({
10421
- email: import_zod51.z.boolean().optional(),
10422
- sms: import_zod51.z.boolean().optional(),
10423
- a2pStatus: import_zod51.z.string().optional(),
10424
- payment: import_zod51.z.string().optional(),
10425
- calendarConnected: import_zod51.z.boolean().optional(),
10426
- social: import_zod51.z.array(import_zod51.z.string()).optional()
10799
+ channels: import_zod52.z.object({
10800
+ email: import_zod52.z.boolean().optional(),
10801
+ sms: import_zod52.z.boolean().optional(),
10802
+ a2pStatus: import_zod52.z.string().optional(),
10803
+ payment: import_zod52.z.string().optional(),
10804
+ calendarConnected: import_zod52.z.boolean().optional(),
10805
+ social: import_zod52.z.array(import_zod52.z.string()).optional()
10427
10806
  }).passthrough().optional(),
10428
- assets: import_zod51.z.object({
10429
- existingPipeline: import_zod51.z.string().optional(),
10430
- existingWorkflows: import_zod51.z.string().optional(),
10431
- brand: import_zod51.z.string().optional(),
10432
- notes: import_zod51.z.string().optional()
10807
+ assets: import_zod52.z.object({
10808
+ existingPipeline: import_zod52.z.string().optional(),
10809
+ existingWorkflows: import_zod52.z.string().optional(),
10810
+ brand: import_zod52.z.string().optional(),
10811
+ notes: import_zod52.z.string().optional()
10433
10812
  }).passthrough().optional(),
10434
- flags: import_zod51.z.array(import_zod51.z.string()).optional()
10813
+ flags: import_zod52.z.array(import_zod52.z.string()).optional()
10435
10814
  }).strict();
10436
10815
  function validateBrief(input) {
10437
10816
  const parsed = briefSchema.safeParse(input);
@@ -10587,7 +10966,7 @@ function normalizeSubmissionToBrief(opts) {
10587
10966
  }
10588
10967
 
10589
10968
  // src/intake-to-build/plan.ts
10590
- var import_zod52 = require("zod");
10969
+ var import_zod53 = require("zod");
10591
10970
  var REF_NAMESPACES = [
10592
10971
  "pipeline",
10593
10972
  "stage",
@@ -10604,22 +10983,22 @@ var REF_NAMESPACES = [
10604
10983
  "handoff"
10605
10984
  ];
10606
10985
  var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
10607
- var refSchema = import_zod52.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
10986
+ var refSchema = import_zod53.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
10608
10987
  function nsRef(ns) {
10609
- return import_zod52.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
10988
+ return import_zod53.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
10610
10989
  }
10611
10990
  function refNamespace(ref) {
10612
10991
  return ref.split(".")[0];
10613
10992
  }
10614
- var stageSchema = import_zod52.z.object({
10993
+ var stageSchema = import_zod53.z.object({
10615
10994
  ref: nsRef("stage"),
10616
- name: import_zod52.z.string(),
10617
- position: import_zod52.z.number().int().nonnegative()
10995
+ name: import_zod53.z.string(),
10996
+ position: import_zod53.z.number().int().nonnegative()
10618
10997
  });
10619
- var pipelineSchema = import_zod52.z.object({
10998
+ var pipelineSchema = import_zod53.z.object({
10620
10999
  ref: nsRef("pipeline"),
10621
- name: import_zod52.z.string(),
10622
- stages: import_zod52.z.array(stageSchema).min(1)
11000
+ name: import_zod53.z.string(),
11001
+ stages: import_zod53.z.array(stageSchema).min(1)
10623
11002
  });
10624
11003
  var GHL_FIELD_DATATYPES = [
10625
11004
  "TEXT",
@@ -10636,21 +11015,21 @@ var GHL_FIELD_DATATYPES = [
10636
11015
  "FILE_UPLOAD",
10637
11016
  "SIGNATURE"
10638
11017
  ];
10639
- var customFieldSchema = import_zod52.z.object({
11018
+ var customFieldSchema = import_zod53.z.object({
10640
11019
  ref: nsRef("field"),
10641
- name: import_zod52.z.string(),
10642
- dataType: import_zod52.z.enum(GHL_FIELD_DATATYPES),
10643
- model: import_zod52.z.enum(["contact", "opportunity"]).optional(),
10644
- options: import_zod52.z.array(import_zod52.z.string()).optional()
11020
+ name: import_zod53.z.string(),
11021
+ dataType: import_zod53.z.enum(GHL_FIELD_DATATYPES),
11022
+ model: import_zod53.z.enum(["contact", "opportunity"]).optional(),
11023
+ options: import_zod53.z.array(import_zod53.z.string()).optional()
10645
11024
  });
10646
- var tagSchema = import_zod52.z.object({
11025
+ var tagSchema = import_zod53.z.object({
10647
11026
  ref: nsRef("tag"),
10648
- name: import_zod52.z.string()
11027
+ name: import_zod53.z.string()
10649
11028
  });
10650
- var customValueSchema = import_zod52.z.object({
11029
+ var customValueSchema = import_zod53.z.object({
10651
11030
  ref: nsRef("cv"),
10652
- name: import_zod52.z.string(),
10653
- value: import_zod52.z.string().optional(),
11031
+ name: import_zod53.z.string(),
11032
+ value: import_zod53.z.string().optional(),
10654
11033
  filledBy: refSchema.optional()
10655
11034
  });
10656
11035
  var CALENDAR_TYPES = [
@@ -10660,142 +11039,142 @@ var CALENDAR_TYPES = [
10660
11039
  "collective",
10661
11040
  "service_booking"
10662
11041
  ];
10663
- var openHoursBlockSchema = import_zod52.z.object({
10664
- daysOfTheWeek: import_zod52.z.array(import_zod52.z.number().int().min(0).max(6)),
10665
- hours: import_zod52.z.array(
10666
- import_zod52.z.object({
10667
- openHour: import_zod52.z.number().int().min(0).max(23),
10668
- openMinute: import_zod52.z.number().int().min(0).max(59),
10669
- closeHour: import_zod52.z.number().int().min(0).max(23),
10670
- closeMinute: import_zod52.z.number().int().min(0).max(59)
11042
+ var openHoursBlockSchema = import_zod53.z.object({
11043
+ daysOfTheWeek: import_zod53.z.array(import_zod53.z.number().int().min(0).max(6)),
11044
+ hours: import_zod53.z.array(
11045
+ import_zod53.z.object({
11046
+ openHour: import_zod53.z.number().int().min(0).max(23),
11047
+ openMinute: import_zod53.z.number().int().min(0).max(59),
11048
+ closeHour: import_zod53.z.number().int().min(0).max(23),
11049
+ closeMinute: import_zod53.z.number().int().min(0).max(59)
10671
11050
  })
10672
11051
  )
10673
11052
  });
10674
- var calendarSchema = import_zod52.z.object({
11053
+ var calendarSchema = import_zod53.z.object({
10675
11054
  ref: nsRef("calendar"),
10676
- name: import_zod52.z.string(),
10677
- calendarType: import_zod52.z.enum(CALENDAR_TYPES),
10678
- openHours: import_zod52.z.array(openHoursBlockSchema).optional(),
10679
- availabilityType: import_zod52.z.number().int().optional(),
10680
- requiresStaff: import_zod52.z.boolean().optional()
11055
+ name: import_zod53.z.string(),
11056
+ calendarType: import_zod53.z.enum(CALENDAR_TYPES),
11057
+ openHours: import_zod53.z.array(openHoursBlockSchema).optional(),
11058
+ availabilityType: import_zod53.z.number().int().optional(),
11059
+ requiresStaff: import_zod53.z.boolean().optional()
10681
11060
  });
10682
- var formFieldSchema = import_zod52.z.discriminatedUnion("type", [
10683
- import_zod52.z.object({
10684
- type: import_zod52.z.literal("standard"),
10685
- key: import_zod52.z.string(),
10686
- required: import_zod52.z.boolean().optional()
11061
+ var formFieldSchema = import_zod53.z.discriminatedUnion("type", [
11062
+ import_zod53.z.object({
11063
+ type: import_zod53.z.literal("standard"),
11064
+ key: import_zod53.z.string(),
11065
+ required: import_zod53.z.boolean().optional()
10687
11066
  }),
10688
- import_zod52.z.object({
10689
- type: import_zod52.z.literal("custom"),
11067
+ import_zod53.z.object({
11068
+ type: import_zod53.z.literal("custom"),
10690
11069
  fieldRef: nsRef("field"),
10691
- required: import_zod52.z.boolean().optional()
11070
+ required: import_zod53.z.boolean().optional()
10692
11071
  })
10693
11072
  ]);
10694
- var formSchema = import_zod52.z.object({
11073
+ var formSchema = import_zod53.z.object({
10695
11074
  ref: nsRef("form"),
10696
- name: import_zod52.z.string(),
10697
- fields: import_zod52.z.array(formFieldSchema)
11075
+ name: import_zod53.z.string(),
11076
+ fields: import_zod53.z.array(formFieldSchema)
10698
11077
  });
10699
- var pageSchema = import_zod52.z.object({
11078
+ var pageSchema = import_zod53.z.object({
10700
11079
  ref: nsRef("page"),
10701
- name: import_zod52.z.string(),
10702
- role: import_zod52.z.string().optional(),
10703
- outline: import_zod52.z.string().optional(),
11080
+ name: import_zod53.z.string(),
11081
+ role: import_zod53.z.string().optional(),
11082
+ outline: import_zod53.z.string().optional(),
10704
11083
  formRef: nsRef("form").optional(),
10705
11084
  calendarRef: nsRef("calendar").optional()
10706
11085
  });
10707
11086
  var FUNNEL_TARGETS = ["ghl", "external"];
10708
11087
  var FUNNEL_HOSTS = ["cloudflare", "vercel"];
10709
- var funnelSchema = import_zod52.z.object({
11088
+ var funnelSchema = import_zod53.z.object({
10710
11089
  ref: nsRef("funnel"),
10711
- name: import_zod52.z.string(),
11090
+ name: import_zod53.z.string(),
10712
11091
  // Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
10713
11092
  // "external" = the subscriber builds + hosts the site themselves (Cloudflare/
10714
11093
  // Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
10715
11094
  // see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
10716
11095
  // deploy an external funnel; it surfaces the GHL-side wiring info.
10717
- target: import_zod52.z.enum(FUNNEL_TARGETS).optional(),
10718
- host: import_zod52.z.enum(FUNNEL_HOSTS).optional(),
11096
+ target: import_zod53.z.enum(FUNNEL_TARGETS).optional(),
11097
+ host: import_zod53.z.enum(FUNNEL_HOSTS).optional(),
10719
11098
  // external only
10720
- domain: import_zod52.z.string().optional(),
11099
+ domain: import_zod53.z.string().optional(),
10721
11100
  // external only
10722
- pages: import_zod52.z.array(pageSchema)
11101
+ pages: import_zod53.z.array(pageSchema)
10723
11102
  });
10724
- var emailAssetSchema = import_zod52.z.object({
11103
+ var emailAssetSchema = import_zod53.z.object({
10725
11104
  ref: nsRef("email"),
10726
- name: import_zod52.z.string(),
10727
- subject: import_zod52.z.string().optional(),
10728
- bodyOutline: import_zod52.z.string().optional(),
10729
- body: import_zod52.z.string().optional(),
10730
- mergeTags: import_zod52.z.array(import_zod52.z.string()).optional()
11105
+ name: import_zod53.z.string(),
11106
+ subject: import_zod53.z.string().optional(),
11107
+ bodyOutline: import_zod53.z.string().optional(),
11108
+ body: import_zod53.z.string().optional(),
11109
+ mergeTags: import_zod53.z.array(import_zod53.z.string()).optional()
10731
11110
  });
10732
- var smsAssetSchema = import_zod52.z.object({
11111
+ var smsAssetSchema = import_zod53.z.object({
10733
11112
  ref: nsRef("sms"),
10734
- name: import_zod52.z.string(),
10735
- bodyOutline: import_zod52.z.string().optional(),
10736
- body: import_zod52.z.string().optional(),
10737
- mergeTags: import_zod52.z.array(import_zod52.z.string()).optional()
11113
+ name: import_zod53.z.string(),
11114
+ bodyOutline: import_zod53.z.string().optional(),
11115
+ body: import_zod53.z.string().optional(),
11116
+ mergeTags: import_zod53.z.array(import_zod53.z.string()).optional()
10738
11117
  });
10739
- var waitUnit = import_zod52.z.enum(["minutes", "hours", "days"]);
10740
- var actionSchema = import_zod52.z.discriminatedUnion("type", [
10741
- import_zod52.z.object({ type: import_zod52.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
10742
- import_zod52.z.object({ type: import_zod52.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
10743
- import_zod52.z.object({ type: import_zod52.z.literal("send_email"), emailRef: nsRef("email") }),
10744
- import_zod52.z.object({ type: import_zod52.z.literal("send_sms"), smsRef: nsRef("sms") }),
10745
- import_zod52.z.object({ type: import_zod52.z.literal("wait"), value: import_zod52.z.number().positive(), unit: waitUnit }),
10746
- import_zod52.z.object({
10747
- type: import_zod52.z.literal("internal_notification"),
10748
- to: import_zod52.z.string(),
10749
- title: import_zod52.z.string(),
10750
- body: import_zod52.z.string()
11118
+ var waitUnit = import_zod53.z.enum(["minutes", "hours", "days"]);
11119
+ var actionSchema = import_zod53.z.discriminatedUnion("type", [
11120
+ import_zod53.z.object({ type: import_zod53.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
11121
+ import_zod53.z.object({ type: import_zod53.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
11122
+ import_zod53.z.object({ type: import_zod53.z.literal("send_email"), emailRef: nsRef("email") }),
11123
+ import_zod53.z.object({ type: import_zod53.z.literal("send_sms"), smsRef: nsRef("sms") }),
11124
+ import_zod53.z.object({ type: import_zod53.z.literal("wait"), value: import_zod53.z.number().positive(), unit: waitUnit }),
11125
+ import_zod53.z.object({
11126
+ type: import_zod53.z.literal("internal_notification"),
11127
+ to: import_zod53.z.string(),
11128
+ title: import_zod53.z.string(),
11129
+ body: import_zod53.z.string()
10751
11130
  }),
10752
- import_zod52.z.object({
10753
- type: import_zod52.z.literal("update_contact_field"),
11131
+ import_zod53.z.object({
11132
+ type: import_zod53.z.literal("update_contact_field"),
10754
11133
  fieldRef: nsRef("field"),
10755
- value: import_zod52.z.string()
11134
+ value: import_zod53.z.string()
10756
11135
  }),
10757
- import_zod52.z.object({ type: import_zod52.z.literal("add_notes"), body: import_zod52.z.string() }),
10758
- import_zod52.z.object({
10759
- type: import_zod52.z.literal("task_notification"),
10760
- title: import_zod52.z.string(),
10761
- body: import_zod52.z.string().optional(),
10762
- dueDate: import_zod52.z.string().optional(),
10763
- assignedTo: import_zod52.z.string().optional()
11136
+ import_zod53.z.object({ type: import_zod53.z.literal("add_notes"), body: import_zod53.z.string() }),
11137
+ import_zod53.z.object({
11138
+ type: import_zod53.z.literal("task_notification"),
11139
+ title: import_zod53.z.string(),
11140
+ body: import_zod53.z.string().optional(),
11141
+ dueDate: import_zod53.z.string().optional(),
11142
+ assignedTo: import_zod53.z.string().optional()
10764
11143
  }),
10765
- import_zod52.z.object({ type: import_zod52.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
10766
- import_zod52.z.object({ type: import_zod52.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
10767
- import_zod52.z.object({
10768
- type: import_zod52.z.literal("create_opportunity"),
11144
+ import_zod53.z.object({ type: import_zod53.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
11145
+ import_zod53.z.object({ type: import_zod53.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
11146
+ import_zod53.z.object({
11147
+ type: import_zod53.z.literal("create_opportunity"),
10769
11148
  pipelineRef: nsRef("pipeline"),
10770
11149
  stageRef: nsRef("stage"),
10771
- status: import_zod52.z.string().optional()
11150
+ status: import_zod53.z.string().optional()
10772
11151
  }),
10773
- import_zod52.z.object({
10774
- type: import_zod52.z.literal("update_opportunity"),
11152
+ import_zod53.z.object({
11153
+ type: import_zod53.z.literal("update_opportunity"),
10775
11154
  pipelineRef: nsRef("pipeline"),
10776
11155
  stageRef: nsRef("stage")
10777
11156
  }),
10778
- import_zod52.z.object({
10779
- type: import_zod52.z.literal("goal_event"),
10780
- goalCondition: import_zod52.z.string(),
11157
+ import_zod53.z.object({
11158
+ type: import_zod53.z.literal("goal_event"),
11159
+ goalCondition: import_zod53.z.string(),
10781
11160
  // GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
10782
- action: import_zod52.z.enum(["exit", "continue", "wait"]).optional()
11161
+ action: import_zod53.z.enum(["exit", "continue", "wait"]).optional()
10783
11162
  })
10784
11163
  ]);
10785
- var triggerSchema = import_zod52.z.object({
10786
- type: import_zod52.z.string(),
11164
+ var triggerSchema = import_zod53.z.object({
11165
+ type: import_zod53.z.string(),
10787
11166
  formRef: nsRef("form").optional(),
10788
11167
  tagRef: nsRef("tag").optional(),
10789
11168
  calendarRef: nsRef("calendar").optional(),
10790
11169
  pipelineRef: nsRef("pipeline").optional(),
10791
11170
  stageRef: nsRef("stage").optional()
10792
11171
  });
10793
- var workflowSchema = import_zod52.z.object({
11172
+ var workflowSchema = import_zod53.z.object({
10794
11173
  ref: nsRef("workflow"),
10795
- name: import_zod52.z.string(),
11174
+ name: import_zod53.z.string(),
10796
11175
  trigger: triggerSchema.optional(),
10797
- stopOnResponse: import_zod52.z.boolean().optional(),
10798
- actions: import_zod52.z.array(actionSchema).max(40)
11176
+ stopOnResponse: import_zod53.z.boolean().optional(),
11177
+ actions: import_zod53.z.array(actionSchema).max(40)
10799
11178
  // house rule: <=40 actions/workflow
10800
11179
  });
10801
11180
  var HANDOFF_OWNER_LEGACY = {
@@ -10803,35 +11182,35 @@ var HANDOFF_OWNER_LEGACY = {
10803
11182
  "JERRY-EXT": "OPERATOR-EXT",
10804
11183
  "SASHA": "TEAM"
10805
11184
  };
10806
- var handoffSchema = import_zod52.z.object({
11185
+ var handoffSchema = import_zod53.z.object({
10807
11186
  ref: nsRef("handoff"),
10808
- owner: import_zod52.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
10809
- title: import_zod52.z.string(),
10810
- trigger: import_zod52.z.string().optional(),
10811
- instruction: import_zod52.z.string(),
11187
+ owner: import_zod53.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
11188
+ title: import_zod53.z.string(),
11189
+ trigger: import_zod53.z.string().optional(),
11190
+ instruction: import_zod53.z.string(),
10812
11191
  produces: refSchema.nullable().optional(),
10813
- successCheck: import_zod52.z.string(),
10814
- blocks: import_zod52.z.array(import_zod52.z.string()).optional()
11192
+ successCheck: import_zod53.z.string(),
11193
+ blocks: import_zod53.z.array(import_zod53.z.string()).optional()
10815
11194
  });
10816
- var buildPlanSchema = import_zod52.z.object({
10817
- schemaVersion: import_zod52.z.string(),
10818
- planId: import_zod52.z.string(),
10819
- briefId: import_zod52.z.string(),
10820
- preset: import_zod52.z.string(),
10821
- summary: import_zod52.z.string().optional(),
10822
- pipelines: import_zod52.z.array(pipelineSchema).optional(),
10823
- customFields: import_zod52.z.array(customFieldSchema).optional(),
10824
- tags: import_zod52.z.array(tagSchema).optional(),
10825
- customValues: import_zod52.z.array(customValueSchema).optional(),
10826
- calendars: import_zod52.z.array(calendarSchema).optional(),
10827
- forms: import_zod52.z.array(formSchema).optional(),
10828
- funnels: import_zod52.z.array(funnelSchema).optional(),
10829
- emails: import_zod52.z.array(emailAssetSchema).optional(),
10830
- sms: import_zod52.z.array(smsAssetSchema).optional(),
10831
- workflows: import_zod52.z.array(workflowSchema).optional(),
10832
- handoffs: import_zod52.z.array(handoffSchema).optional(),
10833
- buildOrder: import_zod52.z.array(import_zod52.z.string()).optional(),
10834
- idMap: import_zod52.z.record(import_zod52.z.string()).optional()
11195
+ var buildPlanSchema = import_zod53.z.object({
11196
+ schemaVersion: import_zod53.z.string(),
11197
+ planId: import_zod53.z.string(),
11198
+ briefId: import_zod53.z.string(),
11199
+ preset: import_zod53.z.string(),
11200
+ summary: import_zod53.z.string().optional(),
11201
+ pipelines: import_zod53.z.array(pipelineSchema).optional(),
11202
+ customFields: import_zod53.z.array(customFieldSchema).optional(),
11203
+ tags: import_zod53.z.array(tagSchema).optional(),
11204
+ customValues: import_zod53.z.array(customValueSchema).optional(),
11205
+ calendars: import_zod53.z.array(calendarSchema).optional(),
11206
+ forms: import_zod53.z.array(formSchema).optional(),
11207
+ funnels: import_zod53.z.array(funnelSchema).optional(),
11208
+ emails: import_zod53.z.array(emailAssetSchema).optional(),
11209
+ sms: import_zod53.z.array(smsAssetSchema).optional(),
11210
+ workflows: import_zod53.z.array(workflowSchema).optional(),
11211
+ handoffs: import_zod53.z.array(handoffSchema).optional(),
11212
+ buildOrder: import_zod53.z.array(import_zod53.z.string()).optional(),
11213
+ idMap: import_zod53.z.record(import_zod53.z.string()).optional()
10835
11214
  }).strict();
10836
11215
  function collectDefinedRefs(plan) {
10837
11216
  const refs = /* @__PURE__ */ new Map();
@@ -11615,10 +11994,81 @@ function renderReport(plan, result, ctx) {
11615
11994
  if (!any) L.push(" (nothing \u2014 everything in this plan is auto-buildable)");
11616
11995
  return L.join("\n");
11617
11996
  }
11997
+ function buildExternalWiring(plan, idMap, locationId2) {
11998
+ const externalFunnels = (plan.funnels ?? []).filter((f) => f.target === "external");
11999
+ if (externalFunnels.length === 0) return void 0;
12000
+ const idx = buildRefIndex(plan);
12001
+ const formByRef = new Map((plan.forms ?? []).map((f) => [f.ref, f]));
12002
+ const triggerTags = [];
12003
+ const seenTriggerTag = /* @__PURE__ */ new Set();
12004
+ const unresolvedTriggerRefs = [];
12005
+ for (const w of plan.workflows ?? []) {
12006
+ const t = w.trigger;
12007
+ if (t && t.type === "contact_tag" && t.tagRef && !seenTriggerTag.has(t.tagRef)) {
12008
+ seenTriggerTag.add(t.tagRef);
12009
+ const id = idMap[t.tagRef];
12010
+ if (id) triggerTags.push({ ref: t.tagRef, name: idx.tagName.get(t.tagRef) ?? t.tagRef, id });
12011
+ else unresolvedTriggerRefs.push(t.tagRef);
12012
+ }
12013
+ }
12014
+ const funnels = externalFunnels.map((fn) => {
12015
+ const formFields = [];
12016
+ const seenField = /* @__PURE__ */ new Set();
12017
+ const unresolved = [];
12018
+ let bookingUrl;
12019
+ for (const pg of fn.pages) {
12020
+ if (pg.formRef) {
12021
+ const form = formByRef.get(pg.formRef);
12022
+ if (!form) {
12023
+ unresolved.push(pg.formRef);
12024
+ } else {
12025
+ for (const fl of form.fields) {
12026
+ if (fl.type === "standard") {
12027
+ const k = `s:${fl.key.toLowerCase()}`;
12028
+ if (seenField.has(k)) continue;
12029
+ seenField.add(k);
12030
+ formFields.push({ kind: "standard", key: fl.key, label: fl.key, required: fl.required ?? false });
12031
+ } else {
12032
+ const k = `c:${fl.fieldRef}`;
12033
+ if (seenField.has(k)) continue;
12034
+ seenField.add(k);
12035
+ const id = idMap[fl.fieldRef];
12036
+ if (!id) unresolved.push(fl.fieldRef);
12037
+ formFields.push({
12038
+ kind: "custom",
12039
+ fieldId: id,
12040
+ // verified; absent ⇒ also listed in `unresolved`
12041
+ label: idx.fieldName.get(fl.fieldRef) ?? fl.fieldRef,
12042
+ dataType: idx.fieldType.get(fl.fieldRef),
12043
+ required: fl.required ?? false
12044
+ });
12045
+ }
12046
+ }
12047
+ }
12048
+ }
12049
+ if (pg.calendarRef && !bookingUrl) {
12050
+ const calId = idMap[pg.calendarRef];
12051
+ if (calId) bookingUrl = `https://api.leadconnectorhq.com/widget/booking/${calId}`;
12052
+ else unresolved.push(pg.calendarRef);
12053
+ }
12054
+ }
12055
+ return {
12056
+ ref: fn.ref,
12057
+ name: fn.name,
12058
+ host: fn.host ?? "cloudflare",
12059
+ domain: fn.domain,
12060
+ formFields,
12061
+ bookingUrl,
12062
+ triggerTags,
12063
+ unresolved: [.../* @__PURE__ */ new Set([...unresolved, ...unresolvedTriggerRefs])]
12064
+ };
12065
+ });
12066
+ return { locationId: locationId2, funnels };
12067
+ }
11618
12068
 
11619
12069
  // src/intake-to-build/execute.ts
11620
12070
  var norm2 = (s) => s.trim().toLowerCase();
11621
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12071
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
11622
12072
  function slugifyName(s) {
11623
12073
  return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
11624
12074
  }
@@ -11641,7 +12091,7 @@ async function executeBackbone(plan, deps, opts = {}) {
11641
12091
  const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
11642
12092
  if (fresh.length === 1) return fresh[0];
11643
12093
  if (fresh.length > 1) return void 0;
11644
- if (attempt < retries) await sleep(backoff * attempt);
12094
+ if (attempt < retries) await sleep2(backoff * attempt);
11645
12095
  }
11646
12096
  return void 0;
11647
12097
  }
@@ -12032,16 +12482,16 @@ function msg(e) {
12032
12482
  }
12033
12483
 
12034
12484
  // src/tools/intake-to-build.ts
12035
- var customFieldItemSchema = import_zod53.z.object({
12036
- id: import_zod53.z.string(),
12037
- name: import_zod53.z.string(),
12038
- fieldKey: import_zod53.z.string(),
12039
- dataType: import_zod53.z.string(),
12040
- model: import_zod53.z.string().optional(),
12041
- parentId: import_zod53.z.string().optional(),
12042
- position: import_zod53.z.number().optional(),
12043
- dateAdded: import_zod53.z.string().optional(),
12044
- picklistOptions: import_zod53.z.array(import_zod53.z.string()).optional()
12485
+ var customFieldItemSchema = import_zod54.z.object({
12486
+ id: import_zod54.z.string(),
12487
+ name: import_zod54.z.string(),
12488
+ fieldKey: import_zod54.z.string(),
12489
+ dataType: import_zod54.z.string(),
12490
+ model: import_zod54.z.string().optional(),
12491
+ parentId: import_zod54.z.string().optional(),
12492
+ position: import_zod54.z.number().optional(),
12493
+ dateAdded: import_zod54.z.string().optional(),
12494
+ picklistOptions: import_zod54.z.array(import_zod54.z.string()).optional()
12045
12495
  }).passthrough();
12046
12496
  function parseCustomFields(raw) {
12047
12497
  const obj = raw && typeof raw === "object" ? raw : {};
@@ -12088,7 +12538,7 @@ function findRecordForQuestion(q, records) {
12088
12538
  const wantName = intakeFieldName(q.label).toLowerCase();
12089
12539
  return records.find((r) => r.name.toLowerCase() === wantName);
12090
12540
  }
12091
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12541
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
12092
12542
  function isFormNotYetPropagated(error) {
12093
12543
  const msg2 = error instanceof Error ? error.message : String(error);
12094
12544
  return /does not exist or is deleted/i.test(msg2);
@@ -12308,7 +12758,7 @@ ${text2.slice(0, 300)}`);
12308
12758
  break;
12309
12759
  } catch (saveErr) {
12310
12760
  if (isFormNotYetPropagated(saveErr) && attempt < 6) {
12311
- await sleep2(700 * attempt);
12761
+ await sleep3(700 * attempt);
12312
12762
  continue;
12313
12763
  }
12314
12764
  throw saveErr;
@@ -12319,7 +12769,7 @@ ${text2.slice(0, 300)}`);
12319
12769
  const verify = await formApiRequest(builderClient, "GET", `/${formId}?locationId=${locationId2}`);
12320
12770
  persisted = countFormFields(verify);
12321
12771
  if (persisted > 0) break;
12322
- if (attempt < 6) await sleep2(700 * attempt);
12772
+ if (attempt < 6) await sleep3(700 * attempt);
12323
12773
  }
12324
12774
  if (persisted === 0) {
12325
12775
  throw new Error(`form "${name}" was created (${formId}) but no fields persisted after save (read-after-write); not binding`);
@@ -12447,7 +12897,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12447
12897
  "validate_brief",
12448
12898
  "Validate an Intake-to-Build Brief object against the \xA74 schema. Returns {valid, errors}. Use to check a normalized brief before handing it to the plan-generation skill.",
12449
12899
  {
12450
- brief: import_zod53.z.record(import_zod53.z.unknown()).describe("The Brief object to validate.")
12900
+ brief: import_zod54.z.record(import_zod54.z.unknown()).describe("The Brief object to validate.")
12451
12901
  },
12452
12902
  async ({ brief }) => validateBrief(brief)
12453
12903
  );
@@ -12456,7 +12906,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12456
12906
  "validate_build_plan",
12457
12907
  "Validate an Intake-to-Build Build Plan against the \xA75 schema AND check ref-integrity: every symbolic ref (pipeline.*, stage.*, tag.*, ...) must resolve to a defined object of the right type, and refs must be unique. Dead references are reported as errors here, before any build runs \u2014 the structural guard against 'an invalid ID silently kills downstream actions'. Returns {valid, errors, warnings, referencesScanned}.",
12458
12908
  {
12459
- plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The Build Plan object to validate.")
12909
+ plan: import_zod54.z.record(import_zod54.z.unknown()).describe("The Build Plan object to validate.")
12460
12910
  },
12461
12911
  async ({ plan }) => validateBuildPlan(plan)
12462
12912
  );
@@ -12464,12 +12914,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12464
12914
  "apply_build_plan",
12465
12915
  `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: a GHL funnel (target:"ghl", default) builds structurally (funnel + named steps; page content/HTML is a manual step \u2014 plans carry outlines); a funnel with target:"external" is NOT built or deployed here \u2014 the user builds + hosts the site themselves (Cloudflare/Vercel) and wires its form back to this GHL sub-account (surfaced as a manual wiring step). Workflows build as DRAFT with all their logical actions expanded to native GHL JSON (incl. opportunity create/move steps, re-enabled v3.41.0) and chained; a contact_tag trigger is built automatically, other trigger types are surfaced as a manual step; the operator reviews + publishes. Always confirms the active location and validates the plan before any write.`,
12466
12916
  {
12467
- plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
12468
- 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)."),
12469
- locationId: import_zod53.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
12470
- 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.'),
12471
- publishWorkflows: import_zod53.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
12472
- onConflict: import_zod53.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
12917
+ plan: import_zod54.z.record(import_zod54.z.unknown()).describe("The approved \xA75 Build Plan object."),
12918
+ mode: import_zod54.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
12919
+ locationId: import_zod54.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
12920
+ metHandoffs: import_zod54.z.array(import_zod54.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.'),
12921
+ publishWorkflows: import_zod54.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
12922
+ onConflict: import_zod54.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
12473
12923
  },
12474
12924
  async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
12475
12925
  try {
@@ -12537,6 +12987,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12537
12987
  }
12538
12988
  const deps = makeExecuteDeps(client, builderClient, activeLocation);
12539
12989
  const exec = await executeBackbone(typedPlan, deps);
12990
+ const externalWiring = buildExternalWiring(typedPlan, exec.idMap, activeLocation);
12540
12991
  const execManualLines = exec.manual.map((m) => `[${m.type}] ${m.reason}`);
12541
12992
  const manualLines = result.workflows.flatMap((w) => [
12542
12993
  ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
@@ -12554,6 +13005,8 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12554
13005
  built: exec.built,
12555
13006
  manual: exec.manual,
12556
13007
  idMap: exec.idMap,
13008
+ externalWiring,
13009
+ externalWiringNote: externalWiring ? "For each target:\"external\" funnel: plug these into your self-hosted site form + lead bridge (templates/external-funnel/). custom formFields carry the VERIFIED GHL field id your form's `custom` object must send (never name-guess keys); add a triggerTag to start the speed-to-lead workflow; bookingUrl is the GHL calendar widget for the CTA. Anything in `unresolved` wasn't built yet \u2014 re-run after it is." : void 0,
12557
13010
  deferred: exec.deferred,
12558
13011
  deferredNote: 'execute builds the WHOLE plan live: CRM backbone (pipelines, custom fields, tags, custom values), calendars, forms, funnels (GHL-built = funnel + named steps), and workflows (DRAFT, with all steps incl. opportunity create/move). Remaining manual steps are surfaced per item: staff-requiring calendars in multi-user accounts, GHL funnel page content/design, EXTERNAL funnels (target:"external" \u2014 the user builds + hosts the site themselves and wires it back; not built here), workflow triggers other than contact_tag, and publishing the DRAFT workflows.',
12559
13012
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
@@ -12649,7 +13102,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12649
13102
  resolved.set(q.key, rec);
12650
13103
  }
12651
13104
  if (!missing) break;
12652
- if (attempt < 6) await sleep2(700 * attempt);
13105
+ if (attempt < 6) await sleep3(700 * attempt);
12653
13106
  }
12654
13107
  if (missing) {
12655
13108
  throw new Error(
@@ -12663,9 +13116,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12663
13116
  "install_intake_form",
12664
13117
  "Install the canonical Intake-to-Build client-intake form into the CURRENT GHL location (whatever get_current_location returns). Account-agnostic, zero hardcoded IDs. Creates any missing intake custom fields (idempotent \u2014 reused on re-run), then builds the form with the proven GHL form-builder field shapes and verifies it. Returns {formId, fieldMap} \u2014 keep fieldMap; normalize_submission_to_brief uses it. Pass dryRun:true to preview what would be created without writing. Pass formId to update an existing intake form in place instead of creating a new one.",
12665
13118
  {
12666
- dryRun: import_zod53.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
12667
- formId: import_zod53.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
12668
- formName: import_zod53.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
13119
+ dryRun: import_zod54.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
13120
+ formId: import_zod54.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
13121
+ formName: import_zod54.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
12669
13122
  },
12670
13123
  async ({ dryRun, formId, formName }) => {
12671
13124
  try {
@@ -12715,7 +13168,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12715
13168
  break;
12716
13169
  } catch (saveErr) {
12717
13170
  if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
12718
- await sleep2(700 * attempt);
13171
+ await sleep3(700 * attempt);
12719
13172
  continue;
12720
13173
  }
12721
13174
  throw saveErr;
@@ -12727,7 +13180,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12727
13180
  const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
12728
13181
  persistedCount = countFormFields(verify);
12729
13182
  if (persistedCount > 0) break;
12730
- if (attempt < 6) await sleep2(700 * attempt);
13183
+ if (attempt < 6) await sleep3(700 * attempt);
12731
13184
  }
12732
13185
  const fieldMap = {};
12733
13186
  for (const [key, rec] of resolved) fieldMap[key] = rec.id;
@@ -12753,10 +13206,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12753
13206
  "normalize_submission_to_brief",
12754
13207
  'Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). Pass the formId; by default the most recent submission is used (or pass submissionId). The intakeKey->customFieldId map is taken from fieldMap if provided (the install_intake_form output, most robust), otherwise reconstructed from the live form. Returns {brief, validation, submissionId} \u2014 validation flags any missing required fields (e.g. an incomplete submission).',
12755
13208
  {
12756
- formId: import_zod53.z.string().describe("The intake form ID (from install_intake_form)."),
12757
- submissionId: import_zod53.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
12758
- fieldMap: import_zod53.z.record(import_zod53.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
12759
- preset: import_zod53.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
13209
+ formId: import_zod54.z.string().describe("The intake form ID (from install_intake_form)."),
13210
+ submissionId: import_zod54.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
13211
+ fieldMap: import_zod54.z.record(import_zod54.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
13212
+ preset: import_zod54.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
12760
13213
  },
12761
13214
  async ({ formId, submissionId, fieldMap, preset }) => {
12762
13215
  try {
@@ -12785,7 +13238,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12785
13238
  const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
12786
13239
  resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
12787
13240
  }
12788
- const presetSchema2 = import_zod53.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
13241
+ const presetSchema2 = import_zod54.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
12789
13242
  const presetParsed = presetSchema2.safeParse(preset);
12790
13243
  const brief = normalizeSubmissionToBrief({
12791
13244
  others,
@@ -12859,11 +13312,13 @@ var LOCATION_SWITCHER_MODULE = "location-switcher";
12859
13312
  var SNAPSHOTS_MODULE = "snapshots";
12860
13313
  var FORM_BUILDER_MODULE = "form-builder";
12861
13314
  var INTAKE_TO_BUILD_MODULE = "intake-to-build";
13315
+ var FUNNEL_QA_MODULE = "funnel-qa";
12862
13316
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
12863
13317
  ...publicApiTools.map(([, label]) => label),
12864
13318
  ...internalApiTools.map(([, label]) => label),
12865
13319
  FORM_BUILDER_MODULE,
12866
13320
  INTAKE_TO_BUILD_MODULE,
13321
+ FUNNEL_QA_MODULE,
12867
13322
  VALIDATORS_MODULE,
12868
13323
  DIAGNOSTICS_MODULE,
12869
13324
  LOCATION_SWITCHER_MODULE,
@@ -12883,6 +13338,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
12883
13338
  }
12884
13339
  registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
12885
13340
  registerIntakeToBuildTools(wrap(INTAKE_TO_BUILD_MODULE), client, builderClient);
13341
+ registerFunnelQaTools(wrap(FUNNEL_QA_MODULE), client, builderClient);
12886
13342
  registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
12887
13343
  registerDiagnosticTools(
12888
13344
  wrap(DIAGNOSTICS_MODULE),