@elitedcs/ghl-mcp 3.45.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.45.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"
@@ -2008,8 +2008,8 @@ function textResponse(text) {
2008
2008
  content: [{ type: "text", text }]
2009
2009
  };
2010
2010
  }
2011
- function escapeRegex(str) {
2012
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2011
+ function escapeRegex(str2) {
2012
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2013
2013
  }
2014
2014
  function errorMessage(error) {
2015
2015
  return error instanceof Error ? error.message : String(error);
@@ -6311,8 +6311,385 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
6311
6311
  );
6312
6312
  }
6313
6313
 
6314
- // src/tools/pipeline-builder.ts
6314
+ // src/tools/funnel-qa.ts
6315
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");
6316
6693
  function registerPipelineBuilderTools(server2, builderClient) {
6317
6694
  const client = builderClient;
6318
6695
  if (!client) return;
@@ -6357,7 +6734,7 @@ ${text2}`);
6357
6734
  "get_pipeline_full",
6358
6735
  "Get a single pipeline with complete stage configuration: IDs, names, positions, display settings.",
6359
6736
  {
6360
- pipelineId: import_zod36.z.string().describe("The pipeline ID to retrieve.")
6737
+ pipelineId: import_zod37.z.string().describe("The pipeline ID to retrieve.")
6361
6738
  },
6362
6739
  async ({ pipelineId }) => {
6363
6740
  try {
@@ -6375,17 +6752,17 @@ ${text2}`);
6375
6752
  "create_pipeline",
6376
6753
  "Create a new pipeline with stages. Each stage needs a name and position (0-based).",
6377
6754
  {
6378
- name: import_zod36.z.string().describe("Pipeline name."),
6379
- stages: import_zod36.z.array(
6380
- import_zod36.z.object({
6381
- name: import_zod36.z.string().describe("Stage name."),
6382
- position: import_zod36.z.number().describe("Stage position (0-based)."),
6383
- showInFunnel: import_zod36.z.boolean().optional().describe("Show in funnel view. Defaults to true."),
6384
- 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.")
6385
6762
  })
6386
6763
  ).describe("Array of stages in order."),
6387
- showInFunnel: import_zod36.z.boolean().optional().describe("Show pipeline in funnel view. Defaults to true."),
6388
- 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.")
6389
6766
  },
6390
6767
  async ({ name, stages, showInFunnel, showInPieChart }) => {
6391
6768
  try {
@@ -6415,19 +6792,19 @@ ${text2}`);
6415
6792
  "update_pipeline",
6416
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.",
6417
6794
  {
6418
- pipelineId: import_zod36.z.string().describe("The pipeline ID to update."),
6419
- name: import_zod36.z.string().optional().describe("New pipeline name."),
6420
- stages: import_zod36.z.array(
6421
- import_zod36.z.object({
6422
- id: import_zod36.z.string().optional().describe("Existing stage ID (omit for new stages)."),
6423
- name: import_zod36.z.string().describe("Stage name."),
6424
- position: import_zod36.z.number().describe("Stage position (0-based)."),
6425
- showInFunnel: import_zod36.z.boolean().optional().describe("Show in funnel view."),
6426
- 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.")
6427
6804
  })
6428
6805
  ).optional().describe("Complete stages array. Stages not included will be removed."),
6429
- showInFunnel: import_zod36.z.boolean().optional().describe("Show pipeline in funnel view."),
6430
- 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.")
6431
6808
  },
6432
6809
  async ({ pipelineId, name, stages, showInFunnel, showInPieChart }) => {
6433
6810
  try {
@@ -6450,8 +6827,8 @@ ${text2}`);
6450
6827
  "delete_pipeline",
6451
6828
  "Permanently delete a pipeline and all its stages. Opportunities become unassigned. IRREVERSIBLE.",
6452
6829
  {
6453
- pipelineId: import_zod36.z.string().describe("The pipeline ID to delete."),
6454
- 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.")
6455
6832
  },
6456
6833
  async ({ pipelineId }) => {
6457
6834
  try {
@@ -6468,12 +6845,12 @@ ${text2}`);
6468
6845
  }
6469
6846
 
6470
6847
  // src/tools/location-switcher.ts
6471
- var import_zod38 = require("zod");
6848
+ var import_zod39 = require("zod");
6472
6849
 
6473
6850
  // src/setup-tool.ts
6474
6851
  var os2 = __toESM(require("os"));
6475
6852
  var crypto2 = __toESM(require("crypto"));
6476
- var import_zod37 = require("zod");
6853
+ var import_zod38 = require("zod");
6477
6854
 
6478
6855
  // src/firebase-capture-script.ts
6479
6856
  var FIREBASE_CAPTURE_SCRIPT = `(async () => {
@@ -6648,19 +7025,19 @@ function registerSetupTool(server2) {
6648
7025
  "setup_ghl_mcp",
6649
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).",
6650
7027
  {
6651
- email: import_zod37.z.string().email().describe("Email used at purchase."),
6652
- license_key: import_zod37.z.string().min(20).describe("License key from your purchase email."),
6653
- 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."),
6654
- 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."),
6655
- 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."),
6656
7033
  // v3.25.0: one-paste shortcut. Run `auto_capture_firebase_script` first;
6657
7034
  // it returns a console script that fills the clipboard with this exact
6658
7035
  // JSON payload. Pasting it here removes the need to fill ghl_user_id,
6659
7036
  // ghl_firebase_api_key, and ghl_firebase_refresh_token individually.
6660
- 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."),
6661
- ghl_user_id: import_zod37.z.string().optional().describe("(Workflow Builder, manual path) Firebase User ID. Prefer firebase_paste instead."),
6662
- ghl_firebase_api_key: import_zod37.z.string().optional().describe("(Workflow Builder, manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste instead."),
6663
- 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.")
6664
7041
  },
6665
7042
  async (args) => {
6666
7043
  const lic = await validateLicense(args.email, args.license_key);
@@ -6761,10 +7138,10 @@ function registerEnableWorkflowBuilderTool(server2) {
6761
7138
  // get the console script; the script returns a JSON object that pastes
6762
7139
  // cleanly into this field. Saves the buyer from picking out three
6763
7140
  // separate fields in IndexedDB.
6764
- 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."),
6765
- ghl_user_id: import_zod37.z.string().min(10).optional().describe("(Manual path) Firebase User ID (uid). Prefer firebase_paste."),
6766
- ghl_firebase_api_key: import_zod37.z.string().min(10).optional().describe("(Manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste."),
6767
- 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.")
6768
7145
  },
6769
7146
  async (args) => {
6770
7147
  const existing = readCredentials();
@@ -6887,8 +7264,8 @@ function registerLeadCaptureTool(server2) {
6887
7264
  "request_license",
6888
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.",
6889
7266
  {
6890
- email: import_zod37.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
6891
- 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).")
6892
7269
  },
6893
7270
  async (args) => {
6894
7271
  const buyUrl = "https://elitedcs.com/ghl-mcp-server";
@@ -7007,7 +7384,7 @@ Token registry: ${registeredCount} location(s) registered${versionLine}`
7007
7384
  "switch_location",
7008
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.",
7009
7386
  {
7010
- 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.")
7011
7388
  },
7012
7389
  async ({ locationId: locationId2 }) => withSwitchLock(async () => {
7013
7390
  const previousId = client.defaultLocationId;
@@ -7080,9 +7457,9 @@ Still on: ${previousId || "none"}${hint}` }],
7080
7457
  "register_location",
7081
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.",
7082
7459
  {
7083
- locationId: import_zod38.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
7084
- name: import_zod38.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
7085
- 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-').")
7086
7463
  },
7087
7464
  async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
7088
7465
  if (!registry2) {
@@ -7139,7 +7516,7 @@ The API key could not access location ${locationId2}. Make sure:
7139
7516
  "register_agency_key",
7140
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.",
7141
7518
  {
7142
- 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.")
7143
7520
  },
7144
7521
  async ({ apiKey: apiKey2 }) => {
7145
7522
  if (!registry2) {
@@ -7193,7 +7570,7 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
7193
7570
  "unregister_location",
7194
7571
  "Remove a GHL sub-account from the token registry.",
7195
7572
  {
7196
- locationId: import_zod38.z.string().describe("The Location ID to remove.")
7573
+ locationId: import_zod39.z.string().describe("The Location ID to remove.")
7197
7574
  },
7198
7575
  async ({ locationId: locationId2 }) => {
7199
7576
  if (!registry2) {
@@ -7219,12 +7596,12 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
7219
7596
  "register_company_firebase",
7220
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.",
7221
7598
  {
7222
- 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."),
7223
- name: import_zod38.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
7224
- 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."),
7225
- 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."),
7226
- 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."),
7227
- 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.")
7228
7605
  },
7229
7606
  async (args) => {
7230
7607
  if (!registry2) {
@@ -7328,7 +7705,7 @@ Now run switch_location to any of this company's sub-accounts \u2014 the workflo
7328
7705
  "unregister_company_firebase",
7329
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.",
7330
7707
  {
7331
- 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.")
7332
7709
  },
7333
7710
  async ({ companyId }) => {
7334
7711
  if (!registry2) {
@@ -7396,8 +7773,8 @@ ${lines.join("\n")}
7396
7773
  "list_available_locations",
7397
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.",
7398
7775
  {
7399
- limit: import_zod38.z.number().optional().describe("Max locations to return. Defaults to 20."),
7400
- 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.")
7401
7778
  },
7402
7779
  async ({ limit, skip }) => {
7403
7780
  try {
@@ -7440,7 +7817,7 @@ ${lines.join("\n")}
7440
7817
  }
7441
7818
 
7442
7819
  // src/tools/bulk-operations.ts
7443
- var import_zod39 = require("zod");
7820
+ var import_zod40 = require("zod");
7444
7821
  function delay(ms) {
7445
7822
  return new Promise((resolve5) => setTimeout(resolve5, ms));
7446
7823
  }
@@ -7452,8 +7829,8 @@ function registerBulkOperationTools(server2, client) {
7452
7829
  "bulk_add_tags",
7453
7830
  "Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
7454
7831
  {
7455
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
7456
- 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.")
7457
7834
  },
7458
7835
  async ({ contactIds, tags }) => {
7459
7836
  const results = { success: 0, failed: 0, errors: [] };
@@ -7475,8 +7852,8 @@ function registerBulkOperationTools(server2, client) {
7475
7852
  "bulk_remove_tags",
7476
7853
  "Remove tags from multiple contacts at once. Rate-limited.",
7477
7854
  {
7478
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
7479
- 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.")
7480
7857
  },
7481
7858
  async ({ contactIds, tags }) => {
7482
7859
  const results = { success: 0, failed: 0, errors: [] };
@@ -7497,8 +7874,8 @@ function registerBulkOperationTools(server2, client) {
7497
7874
  "bulk_update_contacts",
7498
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.",
7499
7876
  {
7500
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
7501
- 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'}).")
7502
7879
  },
7503
7880
  async ({ contactIds, fields }) => {
7504
7881
  const results = { success: 0, failed: 0, errors: [] };
@@ -7519,8 +7896,8 @@ function registerBulkOperationTools(server2, client) {
7519
7896
  "bulk_add_to_workflow",
7520
7897
  "Enroll multiple contacts into a workflow at once. Rate-limited.",
7521
7898
  {
7522
- contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
7523
- 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.")
7524
7901
  },
7525
7902
  async ({ contactIds, workflowId }) => {
7526
7903
  const results = { success: 0, failed: 0, errors: [] };
@@ -7541,8 +7918,8 @@ function registerBulkOperationTools(server2, client) {
7541
7918
  "bulk_delete_contacts",
7542
7919
  "Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
7543
7920
  {
7544
- 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."),
7545
- 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.")
7546
7923
  },
7547
7924
  async ({ contactIds, confirm }) => {
7548
7925
  if (confirm !== "DELETE") {
@@ -7565,7 +7942,7 @@ function registerBulkOperationTools(server2, client) {
7565
7942
  }
7566
7943
 
7567
7944
  // src/tools/account-export.ts
7568
- var import_zod40 = require("zod");
7945
+ var import_zod41 = require("zod");
7569
7946
  function delay2(ms) {
7570
7947
  return new Promise((resolve5) => setTimeout(resolve5, ms));
7571
7948
  }
@@ -7575,8 +7952,8 @@ function registerAccountExportTools(server2, client) {
7575
7952
  "export_account",
7576
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.",
7577
7954
  {
7578
- locationId: import_zod40.z.string().optional().describe("Location ID to export. Uses default if not specified."),
7579
- 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.")
7580
7957
  },
7581
7958
  async ({ locationId: locationId2, includeContacts }) => {
7582
7959
  try {
@@ -7704,8 +8081,8 @@ function registerAccountExportTools(server2, client) {
7704
8081
  "compare_locations",
7705
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.",
7706
8083
  {
7707
- locationA: import_zod40.z.string().describe("First Location ID."),
7708
- 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.")
7709
8086
  },
7710
8087
  async ({ locationA, locationB }) => {
7711
8088
  try {
@@ -7783,7 +8160,7 @@ function registerAccountExportTools(server2, client) {
7783
8160
  }
7784
8161
 
7785
8162
  // src/tools/workflow-cloner.ts
7786
- var import_zod41 = require("zod");
8163
+ var import_zod42 = require("zod");
7787
8164
  var crypto3 = __toESM(require("crypto"));
7788
8165
  function registerWorkflowClonerTools(server2, builderClient) {
7789
8166
  const client = builderClient;
@@ -7792,8 +8169,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
7792
8169
  "clone_workflow",
7793
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.",
7794
8171
  {
7795
- sourceWorkflowId: import_zod41.z.string().describe("The workflow ID to clone."),
7796
- 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.")
7797
8174
  },
7798
8175
  async ({ sourceWorkflowId, newName }) => {
7799
8176
  try {
@@ -7882,7 +8259,7 @@ function registerWorkflowClonerTools(server2, builderClient) {
7882
8259
  }
7883
8260
 
7884
8261
  // src/tools/smart-lists.ts
7885
- var import_zod42 = require("zod");
8262
+ var import_zod43 = require("zod");
7886
8263
  var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
7887
8264
  var OBJECT_KEYS = ["contacts", "opportunity"];
7888
8265
  function registerSmartListTools(server2, builderClient) {
@@ -7909,11 +8286,11 @@ ${text2}`);
7909
8286
  "list_smart_lists",
7910
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.",
7911
8288
  {
7912
- 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."),
7913
- query: import_zod42.z.string().optional().describe("Free-text search across smart list names."),
7914
- limit: import_zod42.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
7915
- startAfter: import_zod42.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
7916
- 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.")
7917
8294
  },
7918
8295
  async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
7919
8296
  try {
@@ -7933,8 +8310,8 @@ ${text2}`);
7933
8310
  "get_smart_list",
7934
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.",
7935
8312
  {
7936
- listId: import_zod42.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
7937
- 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.")
7938
8315
  },
7939
8316
  async ({ listId, locationId: locationId2 }) => {
7940
8317
  try {
@@ -7950,13 +8327,13 @@ ${text2}`);
7950
8327
  "create_smart_list",
7951
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.",
7952
8329
  {
7953
- name: import_zod42.z.string().describe("Display name for the smart list."),
7954
- objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
7955
- 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."),
7956
- 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."),
7957
- 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."),
7958
- defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
7959
- 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.")
7960
8337
  },
7961
8338
  async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
7962
8339
  try {
@@ -7977,13 +8354,13 @@ ${text2}`);
7977
8354
  "update_smart_list",
7978
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.",
7979
8356
  {
7980
- listId: import_zod42.z.string().describe("The smart list ID to update."),
7981
- name: import_zod42.z.string().optional().describe("New display name."),
7982
- 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."),
7983
- columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the column array entirely."),
7984
- pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
7985
- defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
7986
- 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.")
7987
8364
  },
7988
8365
  async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
7989
8366
  try {
@@ -8008,9 +8385,9 @@ ${text2}`);
8008
8385
  "delete_smart_list",
8009
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.",
8010
8387
  {
8011
- listId: import_zod42.z.string().describe("The smart list ID to delete."),
8012
- confirm: import_zod42.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
8013
- 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.")
8014
8391
  },
8015
8392
  async ({ listId, locationId: locationId2 }) => {
8016
8393
  try {
@@ -8025,7 +8402,7 @@ ${text2}`);
8025
8402
  }
8026
8403
 
8027
8404
  // src/tools/reputation.ts
8028
- var import_zod43 = require("zod");
8405
+ var import_zod44 = require("zod");
8029
8406
  var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
8030
8407
  function registerReputationTools(server2, builderClient) {
8031
8408
  const client = builderClient;
@@ -8047,7 +8424,7 @@ ${text2}`);
8047
8424
  "get_review_link_list",
8048
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.",
8049
8426
  {
8050
- 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.")
8051
8428
  },
8052
8429
  async ({ locationId: locationId2 }) => {
8053
8430
  const loc = locationId2 ?? client.locationId;
@@ -8059,11 +8436,11 @@ ${text2}`);
8059
8436
  "list_reviews",
8060
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.",
8061
8438
  {
8062
- locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
8063
- pageNumber: import_zod43.z.number().optional().describe("1-based page number. Defaults to 1."),
8064
- pageSize: import_zod43.z.number().optional().describe("Results per page. Defaults to 10."),
8065
- rating: import_zod43.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
8066
- 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.")
8067
8444
  },
8068
8445
  async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
8069
8446
  const loc = locationId2 ?? client.locationId;
@@ -8093,7 +8470,7 @@ function buildReviewsQuery(locationId2, opts = {}) {
8093
8470
  }
8094
8471
 
8095
8472
  // src/tools/email-campaigns.ts
8096
- var import_zod44 = require("zod");
8473
+ var import_zod45 = require("zod");
8097
8474
  var SVC_BASE = "https://services.leadconnectorhq.com";
8098
8475
  function registerEmailCampaignTools(server2, builderClient) {
8099
8476
  const client = builderClient;
@@ -8103,15 +8480,15 @@ function registerEmailCampaignTools(server2, builderClient) {
8103
8480
  "create_email_campaign",
8104
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.",
8105
8482
  {
8106
- 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."),
8107
- name: import_zod44.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
8108
- subject: import_zod44.z.string().optional().describe("Email subject line recipients see."),
8109
- fromName: import_zod44.z.string().optional().describe("Sender display name."),
8110
- fromEmail: import_zod44.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
8111
- isPlainText: import_zod44.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
8112
- enableResendToUnopened: import_zod44.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
8113
- hasUtmTracking: import_zod44.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
8114
- 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.")
8115
8492
  },
8116
8493
  async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
8117
8494
  const loc = locationId2 ?? client.locationId;
@@ -8145,7 +8522,7 @@ ${text2}`);
8145
8522
  }
8146
8523
 
8147
8524
  // src/tools/memberships.ts
8148
- var import_zod45 = require("zod");
8525
+ var import_zod46 = require("zod");
8149
8526
  var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
8150
8527
  function registerMembershipTools(server2, builderClient) {
8151
8528
  const client = builderClient;
@@ -8171,7 +8548,7 @@ ${text2}`);
8171
8548
  "list_membership_offers",
8172
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.",
8173
8550
  {
8174
- 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.")
8175
8552
  },
8176
8553
  async ({ locationId: locationId2 }) => {
8177
8554
  const loc = locationId2 ?? client.locationId;
@@ -8183,8 +8560,8 @@ ${text2}`);
8183
8560
  "list_membership_categories",
8184
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.",
8185
8562
  {
8186
- limit: import_zod45.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
8187
- 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.")
8188
8565
  },
8189
8566
  async ({ limit, locationId: locationId2 }) => {
8190
8567
  const loc = locationId2 ?? client.locationId;
@@ -8196,8 +8573,8 @@ ${text2}`);
8196
8573
  "list_membership_lessons",
8197
8574
  "List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
8198
8575
  {
8199
- limit: import_zod45.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
8200
- 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.")
8201
8578
  },
8202
8579
  async ({ limit, locationId: locationId2 }) => {
8203
8580
  const loc = locationId2 ?? client.locationId;
@@ -8209,9 +8586,9 @@ ${text2}`);
8209
8586
  "create_course",
8210
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.",
8211
8588
  {
8212
- title: import_zod45.z.string().describe("Course title."),
8213
- description: import_zod45.z.string().optional().describe("Course description. Defaults to empty."),
8214
- 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.")
8215
8592
  },
8216
8593
  async ({ title, description, locationId: locationId2 }) => {
8217
8594
  const loc = locationId2 ?? client.locationId;
@@ -8223,13 +8600,13 @@ ${text2}`);
8223
8600
  "create_membership_category",
8224
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.",
8225
8602
  {
8226
- title: import_zod45.z.string().describe("Category title (e.g. 'Module 1')."),
8227
- productId: import_zod45.z.string().describe("The course/product id this category belongs to."),
8228
- description: import_zod45.z.string().optional().describe("Category description. Defaults to empty."),
8229
- visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8230
- sequenceNo: import_zod45.z.number().optional().describe("Display order within the course. Defaults to 0."),
8231
- dripDays: import_zod45.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
8232
- 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.")
8233
8610
  },
8234
8611
  async ({ title, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
8235
8612
  const loc = locationId2 ?? client.locationId;
@@ -8241,14 +8618,14 @@ ${text2}`);
8241
8618
  "create_membership_lesson",
8242
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.",
8243
8620
  {
8244
- title: import_zod45.z.string().describe("Lesson title."),
8245
- categoryId: import_zod45.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
8246
- productId: import_zod45.z.string().describe("The course/product id this lesson belongs to."),
8247
- description: import_zod45.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
8248
- contentType: import_zod45.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
8249
- visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
8250
- sequenceNo: import_zod45.z.number().optional().describe("Display order within the category. Defaults to 0."),
8251
- 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.")
8252
8629
  },
8253
8630
  async ({ title, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
8254
8631
  const loc = locationId2 ?? client.locationId;
@@ -8260,12 +8637,12 @@ ${text2}`);
8260
8637
  "create_membership_offer",
8261
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.",
8262
8639
  {
8263
- title: import_zod45.z.string().describe("Offer title (shown at checkout / in the offer list)."),
8264
- 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)."),
8265
- type: import_zod45.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
8266
- amount: import_zod45.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
8267
- currency: import_zod45.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
8268
- 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.")
8269
8646
  },
8270
8647
  async ({ title, productIds, type, amount, currency, locationId: locationId2 }) => {
8271
8648
  const loc = locationId2 ?? client.locationId;
@@ -8324,41 +8701,41 @@ function buildOfferPayload(o) {
8324
8701
  }
8325
8702
 
8326
8703
  // src/tools/template-deployer.ts
8327
- var import_zod46 = require("zod");
8704
+ var import_zod47 = require("zod");
8328
8705
  var fs4 = __toESM(require("fs"));
8329
8706
  var path4 = __toESM(require("path"));
8330
8707
  function delay3(ms) {
8331
8708
  return new Promise((resolve5) => setTimeout(resolve5, ms));
8332
8709
  }
8333
- var TemplateSchema = import_zod46.z.object({
8334
- templateName: import_zod46.z.string(),
8335
- templateVersion: import_zod46.z.string().optional(),
8336
- description: import_zod46.z.string().optional().default(""),
8337
- questionnaire: import_zod46.z.array(import_zod46.z.object({
8338
- id: import_zod46.z.string(),
8339
- question: import_zod46.z.string(),
8340
- type: import_zod46.z.string(),
8341
- required: import_zod46.z.boolean().optional(),
8342
- 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()
8343
8720
  })).optional().default([]),
8344
- location: import_zod46.z.record(import_zod46.z.unknown()).optional(),
8345
- tags: import_zod46.z.array(import_zod46.z.string()).optional(),
8346
- customFields: import_zod46.z.array(import_zod46.z.object({
8347
- name: import_zod46.z.string(),
8348
- 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()
8349
8726
  })).optional(),
8350
- pipelines: import_zod46.z.array(import_zod46.z.object({
8351
- name: import_zod46.z.string(),
8352
- 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() }))
8353
8730
  })).optional(),
8354
- workflows: import_zod46.z.array(import_zod46.z.object({
8355
- name: import_zod46.z.string(),
8356
- condition: import_zod46.z.string().optional(),
8357
- 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([])
8358
8735
  })).optional(),
8359
- calendars: import_zod46.z.array(import_zod46.z.object({
8360
- name: import_zod46.z.string(),
8361
- 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()
8362
8739
  })).optional()
8363
8740
  });
8364
8741
  function registerTemplateDeployerTools(server2, client) {
@@ -8429,7 +8806,7 @@ function registerTemplateDeployerTools(server2, client) {
8429
8806
  "get_template_questionnaire",
8430
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.",
8431
8808
  {
8432
- 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).")
8433
8810
  },
8434
8811
  async ({ templateFile }) => {
8435
8812
  try {
@@ -8462,10 +8839,10 @@ function registerTemplateDeployerTools(server2, client) {
8462
8839
  "deploy_template",
8463
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.",
8464
8841
  {
8465
- templateFile: import_zod46.z.string().describe("Path to the template JSON file."),
8466
- 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', ...})."),
8467
- locationId: import_zod46.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
8468
- 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.")
8469
8846
  },
8470
8847
  async ({ templateFile, answers, locationId: locationId2, dryRun }) => {
8471
8848
  try {
@@ -8711,7 +9088,7 @@ ${errors.join("\n")}` : "\nNo errors!",
8711
9088
  }
8712
9089
 
8713
9090
  // src/tools/validators.ts
8714
- var import_zod47 = require("zod");
9091
+ var import_zod48 = require("zod");
8715
9092
  var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
8716
9093
  var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
8717
9094
  "first_name",
@@ -9169,7 +9546,7 @@ function registerValidatorTools(server2, client, builderClient) {
9169
9546
  server2.tool(
9170
9547
  "validate_workflow",
9171
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'.",
9172
- { workflowId: import_zod47.z.string().describe("The workflow ID to validate.") },
9549
+ { workflowId: import_zod48.z.string().describe("The workflow ID to validate.") },
9173
9550
  async ({ workflowId }) => {
9174
9551
  try {
9175
9552
  const workflow = await builderClient.getWorkflow(workflowId);
@@ -9421,10 +9798,10 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
9421
9798
  }
9422
9799
 
9423
9800
  // src/tools/snapshots.ts
9424
- var import_zod48 = require("zod");
9425
- var SnapshotSchema = import_zod48.z.object({ id: import_zod48.z.string(), name: import_zod48.z.string(), type: import_zod48.z.string() }).passthrough();
9426
- var SnapshotsResponseSchema = import_zod48.z.object({ snapshots: import_zod48.z.array(SnapshotSchema) }).passthrough();
9427
- 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();
9428
9805
  var SHARE_TYPES = [
9429
9806
  "link",
9430
9807
  "permanent_link",
@@ -9471,7 +9848,7 @@ function registerSnapshotTools(server2, client, registry2) {
9471
9848
  "list_snapshots",
9472
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.",
9473
9850
  {
9474
- companyId: import_zod48.z.string().optional().describe(
9851
+ companyId: import_zod49.z.string().optional().describe(
9475
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."
9476
9853
  )
9477
9854
  },
@@ -9496,11 +9873,11 @@ function registerSnapshotTools(server2, client, registry2) {
9496
9873
  "create_snapshot_share_link",
9497
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.",
9498
9875
  {
9499
- snapshot_id: import_zod48.z.string().describe("The snapshot id to share (from list_snapshots)."),
9500
- 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(
9501
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."
9502
9879
  ),
9503
- companyId: import_zod48.z.string().optional().describe(
9880
+ companyId: import_zod49.z.string().optional().describe(
9504
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."
9505
9882
  )
9506
9883
  },
@@ -9530,17 +9907,17 @@ function registerSnapshotTools(server2, client, registry2) {
9530
9907
  }
9531
9908
 
9532
9909
  // src/tools/phone.ts
9533
- var import_zod49 = require("zod");
9534
- var PhoneNumberSchema = import_zod49.z.object({ sid: import_zod49.z.string(), value: import_zod49.z.string(), title: import_zod49.z.string().optional() }).passthrough();
9535
- var NumbersResponseSchema = import_zod49.z.object({ phoneNumbers: import_zod49.z.array(PhoneNumberSchema) }).passthrough();
9536
- 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();
9537
9914
  function registerPhoneTools(server2, client) {
9538
9915
  safeTool(
9539
9916
  server2,
9540
9917
  "list_phone_numbers",
9541
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).",
9542
9919
  {
9543
- 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.")
9544
9921
  },
9545
9922
  async ({ locationId: locationId2 }) => {
9546
9923
  const loc = client.resolveLocationId(locationId2);
@@ -9558,7 +9935,7 @@ function registerPhoneTools(server2, client) {
9558
9935
  "list_number_pools",
9559
9936
  "List LC Phone number pools configured for a location. Read-only.",
9560
9937
  {
9561
- 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.")
9562
9939
  },
9563
9940
  async ({ locationId: locationId2 }) => {
9564
9941
  const loc = client.resolveLocationId(locationId2);
@@ -9570,10 +9947,10 @@ function registerPhoneTools(server2, client) {
9570
9947
  }
9571
9948
 
9572
9949
  // src/tools/account-health.ts
9573
- var import_zod50 = require("zod");
9574
- var MetaTotalSchema = import_zod50.z.object({ meta: import_zod50.z.object({ total: import_zod50.z.number() }).passthrough() }).passthrough();
9575
- var TotalSchema = import_zod50.z.object({ total: import_zod50.z.number() }).passthrough();
9576
- 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();
9577
9954
  var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
9578
9955
  async function section(scope, fn) {
9579
9956
  try {
@@ -9588,8 +9965,8 @@ function registerAccountHealthTools(server2, client) {
9588
9965
  "get_account_health_summary",
9589
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).",
9590
9967
  {
9591
- locationId: import_zod50.z.string().optional().describe("Defaults to the active location."),
9592
- 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.")
9593
9970
  },
9594
9971
  async ({ locationId: locationId2, windowDays }) => {
9595
9972
  const loc = client.resolveLocationId(locationId2);
@@ -9661,7 +10038,7 @@ function registerAccountHealthTools(server2, client) {
9661
10038
  }
9662
10039
 
9663
10040
  // src/tools/intake-to-build.ts
9664
- var import_zod53 = require("zod");
10041
+ var import_zod54 = require("zod");
9665
10042
 
9666
10043
  // src/intake-to-build/question-set.ts
9667
10044
  var QUESTION_SET_VERSION = "0.1";
@@ -10363,7 +10740,7 @@ function buildPlanFormData(fields, locationId2) {
10363
10740
  }
10364
10741
 
10365
10742
  // src/intake-to-build/brief.ts
10366
- var import_zod51 = require("zod");
10743
+ var import_zod52 = require("zod");
10367
10744
  var BRIEF_SCHEMA_VERSION = "0.1";
10368
10745
  var PRESETS = [
10369
10746
  "generic",
@@ -10373,67 +10750,67 @@ var PRESETS = [
10373
10750
  "ecom",
10374
10751
  "agency"
10375
10752
  ];
10376
- var presetSchema = import_zod51.z.enum(PRESETS);
10753
+ var presetSchema = import_zod52.z.enum(PRESETS);
10377
10754
  var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
10378
- var briefSourceSchema = import_zod51.z.enum(BRIEF_SOURCES);
10379
- var pricePointSchema = import_zod51.z.object({
10380
- name: import_zod51.z.string(),
10381
- 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()
10382
10759
  });
10383
- var briefSchema = import_zod51.z.object({
10384
- schemaVersion: import_zod51.z.string(),
10385
- briefId: import_zod51.z.string(),
10760
+ var briefSchema = import_zod52.z.object({
10761
+ schemaVersion: import_zod52.z.string(),
10762
+ briefId: import_zod52.z.string(),
10386
10763
  preset: presetSchema,
10387
10764
  briefSource: briefSourceSchema,
10388
10765
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
10389
- extended: import_zod51.z.record(import_zod51.z.unknown()).optional(),
10390
- business: import_zod51.z.object({
10391
- name: import_zod51.z.string(),
10392
- type: import_zod51.z.string().optional(),
10393
- website: import_zod51.z.string().optional(),
10394
- location: import_zod51.z.string().optional(),
10395
- 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(),
10396
10773
  // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
10397
10774
  // the same tolerance reason as business.type (don't reject valid briefs).
10398
- teamSize: import_zod51.z.string().optional(),
10399
- monthlyLeadVolume: import_zod51.z.string().optional(),
10400
- 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()
10401
10778
  }).passthrough(),
10402
- offer: import_zod51.z.object({
10403
- summary: import_zod51.z.string().optional(),
10779
+ offer: import_zod52.z.object({
10780
+ summary: import_zod52.z.string().optional(),
10404
10781
  // Parsed best-effort; tolerate a raw string when parsing was not possible.
10405
- pricePoints: import_zod51.z.union([import_zod51.z.array(pricePointSchema), import_zod51.z.string()]).optional(),
10406
- leadMagnet: import_zod51.z.string().optional(),
10407
- 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()
10408
10785
  }).passthrough().optional(),
10409
- audience: import_zod51.z.object({
10410
- ideal: import_zod51.z.string().optional(),
10411
- painPoints: import_zod51.z.array(import_zod51.z.string()).optional(),
10412
- 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()
10413
10790
  }).passthrough().optional(),
10414
- goal: import_zod51.z.object({
10791
+ goal: import_zod52.z.object({
10415
10792
  // Kept as string: the form option labels are the canonical values, but
10416
10793
  // the contract sample shortens some (e.g. "high-touch"). See §7 note.
10417
- primary: import_zod51.z.string(),
10418
- salesStages: import_zod51.z.array(import_zod51.z.string()).optional(),
10419
- bookingNeeded: import_zod51.z.boolean().optional(),
10420
- 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()
10421
10798
  }).passthrough(),
10422
- channels: import_zod51.z.object({
10423
- email: import_zod51.z.boolean().optional(),
10424
- sms: import_zod51.z.boolean().optional(),
10425
- a2pStatus: import_zod51.z.string().optional(),
10426
- payment: import_zod51.z.string().optional(),
10427
- calendarConnected: import_zod51.z.boolean().optional(),
10428
- 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()
10429
10806
  }).passthrough().optional(),
10430
- assets: import_zod51.z.object({
10431
- existingPipeline: import_zod51.z.string().optional(),
10432
- existingWorkflows: import_zod51.z.string().optional(),
10433
- brand: import_zod51.z.string().optional(),
10434
- 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()
10435
10812
  }).passthrough().optional(),
10436
- flags: import_zod51.z.array(import_zod51.z.string()).optional()
10813
+ flags: import_zod52.z.array(import_zod52.z.string()).optional()
10437
10814
  }).strict();
10438
10815
  function validateBrief(input) {
10439
10816
  const parsed = briefSchema.safeParse(input);
@@ -10589,7 +10966,7 @@ function normalizeSubmissionToBrief(opts) {
10589
10966
  }
10590
10967
 
10591
10968
  // src/intake-to-build/plan.ts
10592
- var import_zod52 = require("zod");
10969
+ var import_zod53 = require("zod");
10593
10970
  var REF_NAMESPACES = [
10594
10971
  "pipeline",
10595
10972
  "stage",
@@ -10606,22 +10983,22 @@ var REF_NAMESPACES = [
10606
10983
  "handoff"
10607
10984
  ];
10608
10985
  var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
10609
- 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)");
10610
10987
  function nsRef(ns) {
10611
- 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`);
10612
10989
  }
10613
10990
  function refNamespace(ref) {
10614
10991
  return ref.split(".")[0];
10615
10992
  }
10616
- var stageSchema = import_zod52.z.object({
10993
+ var stageSchema = import_zod53.z.object({
10617
10994
  ref: nsRef("stage"),
10618
- name: import_zod52.z.string(),
10619
- position: import_zod52.z.number().int().nonnegative()
10995
+ name: import_zod53.z.string(),
10996
+ position: import_zod53.z.number().int().nonnegative()
10620
10997
  });
10621
- var pipelineSchema = import_zod52.z.object({
10998
+ var pipelineSchema = import_zod53.z.object({
10622
10999
  ref: nsRef("pipeline"),
10623
- name: import_zod52.z.string(),
10624
- stages: import_zod52.z.array(stageSchema).min(1)
11000
+ name: import_zod53.z.string(),
11001
+ stages: import_zod53.z.array(stageSchema).min(1)
10625
11002
  });
10626
11003
  var GHL_FIELD_DATATYPES = [
10627
11004
  "TEXT",
@@ -10638,21 +11015,21 @@ var GHL_FIELD_DATATYPES = [
10638
11015
  "FILE_UPLOAD",
10639
11016
  "SIGNATURE"
10640
11017
  ];
10641
- var customFieldSchema = import_zod52.z.object({
11018
+ var customFieldSchema = import_zod53.z.object({
10642
11019
  ref: nsRef("field"),
10643
- name: import_zod52.z.string(),
10644
- dataType: import_zod52.z.enum(GHL_FIELD_DATATYPES),
10645
- model: import_zod52.z.enum(["contact", "opportunity"]).optional(),
10646
- 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()
10647
11024
  });
10648
- var tagSchema = import_zod52.z.object({
11025
+ var tagSchema = import_zod53.z.object({
10649
11026
  ref: nsRef("tag"),
10650
- name: import_zod52.z.string()
11027
+ name: import_zod53.z.string()
10651
11028
  });
10652
- var customValueSchema = import_zod52.z.object({
11029
+ var customValueSchema = import_zod53.z.object({
10653
11030
  ref: nsRef("cv"),
10654
- name: import_zod52.z.string(),
10655
- value: import_zod52.z.string().optional(),
11031
+ name: import_zod53.z.string(),
11032
+ value: import_zod53.z.string().optional(),
10656
11033
  filledBy: refSchema.optional()
10657
11034
  });
10658
11035
  var CALENDAR_TYPES = [
@@ -10662,142 +11039,142 @@ var CALENDAR_TYPES = [
10662
11039
  "collective",
10663
11040
  "service_booking"
10664
11041
  ];
10665
- var openHoursBlockSchema = import_zod52.z.object({
10666
- daysOfTheWeek: import_zod52.z.array(import_zod52.z.number().int().min(0).max(6)),
10667
- hours: import_zod52.z.array(
10668
- import_zod52.z.object({
10669
- openHour: import_zod52.z.number().int().min(0).max(23),
10670
- openMinute: import_zod52.z.number().int().min(0).max(59),
10671
- closeHour: import_zod52.z.number().int().min(0).max(23),
10672
- 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)
10673
11050
  })
10674
11051
  )
10675
11052
  });
10676
- var calendarSchema = import_zod52.z.object({
11053
+ var calendarSchema = import_zod53.z.object({
10677
11054
  ref: nsRef("calendar"),
10678
- name: import_zod52.z.string(),
10679
- calendarType: import_zod52.z.enum(CALENDAR_TYPES),
10680
- openHours: import_zod52.z.array(openHoursBlockSchema).optional(),
10681
- availabilityType: import_zod52.z.number().int().optional(),
10682
- 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()
10683
11060
  });
10684
- var formFieldSchema = import_zod52.z.discriminatedUnion("type", [
10685
- import_zod52.z.object({
10686
- type: import_zod52.z.literal("standard"),
10687
- key: import_zod52.z.string(),
10688
- 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()
10689
11066
  }),
10690
- import_zod52.z.object({
10691
- type: import_zod52.z.literal("custom"),
11067
+ import_zod53.z.object({
11068
+ type: import_zod53.z.literal("custom"),
10692
11069
  fieldRef: nsRef("field"),
10693
- required: import_zod52.z.boolean().optional()
11070
+ required: import_zod53.z.boolean().optional()
10694
11071
  })
10695
11072
  ]);
10696
- var formSchema = import_zod52.z.object({
11073
+ var formSchema = import_zod53.z.object({
10697
11074
  ref: nsRef("form"),
10698
- name: import_zod52.z.string(),
10699
- fields: import_zod52.z.array(formFieldSchema)
11075
+ name: import_zod53.z.string(),
11076
+ fields: import_zod53.z.array(formFieldSchema)
10700
11077
  });
10701
- var pageSchema = import_zod52.z.object({
11078
+ var pageSchema = import_zod53.z.object({
10702
11079
  ref: nsRef("page"),
10703
- name: import_zod52.z.string(),
10704
- role: import_zod52.z.string().optional(),
10705
- 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(),
10706
11083
  formRef: nsRef("form").optional(),
10707
11084
  calendarRef: nsRef("calendar").optional()
10708
11085
  });
10709
11086
  var FUNNEL_TARGETS = ["ghl", "external"];
10710
11087
  var FUNNEL_HOSTS = ["cloudflare", "vercel"];
10711
- var funnelSchema = import_zod52.z.object({
11088
+ var funnelSchema = import_zod53.z.object({
10712
11089
  ref: nsRef("funnel"),
10713
- name: import_zod52.z.string(),
11090
+ name: import_zod53.z.string(),
10714
11091
  // Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
10715
11092
  // "external" = the subscriber builds + hosts the site themselves (Cloudflare/
10716
11093
  // Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
10717
11094
  // see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
10718
11095
  // deploy an external funnel; it surfaces the GHL-side wiring info.
10719
- target: import_zod52.z.enum(FUNNEL_TARGETS).optional(),
10720
- 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(),
10721
11098
  // external only
10722
- domain: import_zod52.z.string().optional(),
11099
+ domain: import_zod53.z.string().optional(),
10723
11100
  // external only
10724
- pages: import_zod52.z.array(pageSchema)
11101
+ pages: import_zod53.z.array(pageSchema)
10725
11102
  });
10726
- var emailAssetSchema = import_zod52.z.object({
11103
+ var emailAssetSchema = import_zod53.z.object({
10727
11104
  ref: nsRef("email"),
10728
- name: import_zod52.z.string(),
10729
- subject: import_zod52.z.string().optional(),
10730
- bodyOutline: import_zod52.z.string().optional(),
10731
- body: import_zod52.z.string().optional(),
10732
- 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()
10733
11110
  });
10734
- var smsAssetSchema = import_zod52.z.object({
11111
+ var smsAssetSchema = import_zod53.z.object({
10735
11112
  ref: nsRef("sms"),
10736
- name: import_zod52.z.string(),
10737
- bodyOutline: import_zod52.z.string().optional(),
10738
- body: import_zod52.z.string().optional(),
10739
- 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()
10740
11117
  });
10741
- var waitUnit = import_zod52.z.enum(["minutes", "hours", "days"]);
10742
- var actionSchema = import_zod52.z.discriminatedUnion("type", [
10743
- import_zod52.z.object({ type: import_zod52.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
10744
- import_zod52.z.object({ type: import_zod52.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
10745
- import_zod52.z.object({ type: import_zod52.z.literal("send_email"), emailRef: nsRef("email") }),
10746
- import_zod52.z.object({ type: import_zod52.z.literal("send_sms"), smsRef: nsRef("sms") }),
10747
- import_zod52.z.object({ type: import_zod52.z.literal("wait"), value: import_zod52.z.number().positive(), unit: waitUnit }),
10748
- import_zod52.z.object({
10749
- type: import_zod52.z.literal("internal_notification"),
10750
- to: import_zod52.z.string(),
10751
- title: import_zod52.z.string(),
10752
- 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()
10753
11130
  }),
10754
- import_zod52.z.object({
10755
- type: import_zod52.z.literal("update_contact_field"),
11131
+ import_zod53.z.object({
11132
+ type: import_zod53.z.literal("update_contact_field"),
10756
11133
  fieldRef: nsRef("field"),
10757
- value: import_zod52.z.string()
11134
+ value: import_zod53.z.string()
10758
11135
  }),
10759
- import_zod52.z.object({ type: import_zod52.z.literal("add_notes"), body: import_zod52.z.string() }),
10760
- import_zod52.z.object({
10761
- type: import_zod52.z.literal("task_notification"),
10762
- title: import_zod52.z.string(),
10763
- body: import_zod52.z.string().optional(),
10764
- dueDate: import_zod52.z.string().optional(),
10765
- 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()
10766
11143
  }),
10767
- import_zod52.z.object({ type: import_zod52.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
10768
- import_zod52.z.object({ type: import_zod52.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
10769
- import_zod52.z.object({
10770
- 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"),
10771
11148
  pipelineRef: nsRef("pipeline"),
10772
11149
  stageRef: nsRef("stage"),
10773
- status: import_zod52.z.string().optional()
11150
+ status: import_zod53.z.string().optional()
10774
11151
  }),
10775
- import_zod52.z.object({
10776
- type: import_zod52.z.literal("update_opportunity"),
11152
+ import_zod53.z.object({
11153
+ type: import_zod53.z.literal("update_opportunity"),
10777
11154
  pipelineRef: nsRef("pipeline"),
10778
11155
  stageRef: nsRef("stage")
10779
11156
  }),
10780
- import_zod52.z.object({
10781
- type: import_zod52.z.literal("goal_event"),
10782
- goalCondition: import_zod52.z.string(),
11157
+ import_zod53.z.object({
11158
+ type: import_zod53.z.literal("goal_event"),
11159
+ goalCondition: import_zod53.z.string(),
10783
11160
  // GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
10784
- action: import_zod52.z.enum(["exit", "continue", "wait"]).optional()
11161
+ action: import_zod53.z.enum(["exit", "continue", "wait"]).optional()
10785
11162
  })
10786
11163
  ]);
10787
- var triggerSchema = import_zod52.z.object({
10788
- type: import_zod52.z.string(),
11164
+ var triggerSchema = import_zod53.z.object({
11165
+ type: import_zod53.z.string(),
10789
11166
  formRef: nsRef("form").optional(),
10790
11167
  tagRef: nsRef("tag").optional(),
10791
11168
  calendarRef: nsRef("calendar").optional(),
10792
11169
  pipelineRef: nsRef("pipeline").optional(),
10793
11170
  stageRef: nsRef("stage").optional()
10794
11171
  });
10795
- var workflowSchema = import_zod52.z.object({
11172
+ var workflowSchema = import_zod53.z.object({
10796
11173
  ref: nsRef("workflow"),
10797
- name: import_zod52.z.string(),
11174
+ name: import_zod53.z.string(),
10798
11175
  trigger: triggerSchema.optional(),
10799
- stopOnResponse: import_zod52.z.boolean().optional(),
10800
- actions: import_zod52.z.array(actionSchema).max(40)
11176
+ stopOnResponse: import_zod53.z.boolean().optional(),
11177
+ actions: import_zod53.z.array(actionSchema).max(40)
10801
11178
  // house rule: <=40 actions/workflow
10802
11179
  });
10803
11180
  var HANDOFF_OWNER_LEGACY = {
@@ -10805,35 +11182,35 @@ var HANDOFF_OWNER_LEGACY = {
10805
11182
  "JERRY-EXT": "OPERATOR-EXT",
10806
11183
  "SASHA": "TEAM"
10807
11184
  };
10808
- var handoffSchema = import_zod52.z.object({
11185
+ var handoffSchema = import_zod53.z.object({
10809
11186
  ref: nsRef("handoff"),
10810
- owner: import_zod52.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
10811
- title: import_zod52.z.string(),
10812
- trigger: import_zod52.z.string().optional(),
10813
- 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(),
10814
11191
  produces: refSchema.nullable().optional(),
10815
- successCheck: import_zod52.z.string(),
10816
- 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()
10817
11194
  });
10818
- var buildPlanSchema = import_zod52.z.object({
10819
- schemaVersion: import_zod52.z.string(),
10820
- planId: import_zod52.z.string(),
10821
- briefId: import_zod52.z.string(),
10822
- preset: import_zod52.z.string(),
10823
- summary: import_zod52.z.string().optional(),
10824
- pipelines: import_zod52.z.array(pipelineSchema).optional(),
10825
- customFields: import_zod52.z.array(customFieldSchema).optional(),
10826
- tags: import_zod52.z.array(tagSchema).optional(),
10827
- customValues: import_zod52.z.array(customValueSchema).optional(),
10828
- calendars: import_zod52.z.array(calendarSchema).optional(),
10829
- forms: import_zod52.z.array(formSchema).optional(),
10830
- funnels: import_zod52.z.array(funnelSchema).optional(),
10831
- emails: import_zod52.z.array(emailAssetSchema).optional(),
10832
- sms: import_zod52.z.array(smsAssetSchema).optional(),
10833
- workflows: import_zod52.z.array(workflowSchema).optional(),
10834
- handoffs: import_zod52.z.array(handoffSchema).optional(),
10835
- buildOrder: import_zod52.z.array(import_zod52.z.string()).optional(),
10836
- 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()
10837
11214
  }).strict();
10838
11215
  function collectDefinedRefs(plan) {
10839
11216
  const refs = /* @__PURE__ */ new Map();
@@ -11617,10 +11994,81 @@ function renderReport(plan, result, ctx) {
11617
11994
  if (!any) L.push(" (nothing \u2014 everything in this plan is auto-buildable)");
11618
11995
  return L.join("\n");
11619
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
+ }
11620
12068
 
11621
12069
  // src/intake-to-build/execute.ts
11622
12070
  var norm2 = (s) => s.trim().toLowerCase();
11623
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12071
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
11624
12072
  function slugifyName(s) {
11625
12073
  return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
11626
12074
  }
@@ -11643,7 +12091,7 @@ async function executeBackbone(plan, deps, opts = {}) {
11643
12091
  const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
11644
12092
  if (fresh.length === 1) return fresh[0];
11645
12093
  if (fresh.length > 1) return void 0;
11646
- if (attempt < retries) await sleep(backoff * attempt);
12094
+ if (attempt < retries) await sleep2(backoff * attempt);
11647
12095
  }
11648
12096
  return void 0;
11649
12097
  }
@@ -12034,16 +12482,16 @@ function msg(e) {
12034
12482
  }
12035
12483
 
12036
12484
  // src/tools/intake-to-build.ts
12037
- var customFieldItemSchema = import_zod53.z.object({
12038
- id: import_zod53.z.string(),
12039
- name: import_zod53.z.string(),
12040
- fieldKey: import_zod53.z.string(),
12041
- dataType: import_zod53.z.string(),
12042
- model: import_zod53.z.string().optional(),
12043
- parentId: import_zod53.z.string().optional(),
12044
- position: import_zod53.z.number().optional(),
12045
- dateAdded: import_zod53.z.string().optional(),
12046
- 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()
12047
12495
  }).passthrough();
12048
12496
  function parseCustomFields(raw) {
12049
12497
  const obj = raw && typeof raw === "object" ? raw : {};
@@ -12090,7 +12538,7 @@ function findRecordForQuestion(q, records) {
12090
12538
  const wantName = intakeFieldName(q.label).toLowerCase();
12091
12539
  return records.find((r) => r.name.toLowerCase() === wantName);
12092
12540
  }
12093
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12541
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
12094
12542
  function isFormNotYetPropagated(error) {
12095
12543
  const msg2 = error instanceof Error ? error.message : String(error);
12096
12544
  return /does not exist or is deleted/i.test(msg2);
@@ -12310,7 +12758,7 @@ ${text2.slice(0, 300)}`);
12310
12758
  break;
12311
12759
  } catch (saveErr) {
12312
12760
  if (isFormNotYetPropagated(saveErr) && attempt < 6) {
12313
- await sleep2(700 * attempt);
12761
+ await sleep3(700 * attempt);
12314
12762
  continue;
12315
12763
  }
12316
12764
  throw saveErr;
@@ -12321,7 +12769,7 @@ ${text2.slice(0, 300)}`);
12321
12769
  const verify = await formApiRequest(builderClient, "GET", `/${formId}?locationId=${locationId2}`);
12322
12770
  persisted = countFormFields(verify);
12323
12771
  if (persisted > 0) break;
12324
- if (attempt < 6) await sleep2(700 * attempt);
12772
+ if (attempt < 6) await sleep3(700 * attempt);
12325
12773
  }
12326
12774
  if (persisted === 0) {
12327
12775
  throw new Error(`form "${name}" was created (${formId}) but no fields persisted after save (read-after-write); not binding`);
@@ -12449,7 +12897,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12449
12897
  "validate_brief",
12450
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.",
12451
12899
  {
12452
- 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.")
12453
12901
  },
12454
12902
  async ({ brief }) => validateBrief(brief)
12455
12903
  );
@@ -12458,7 +12906,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12458
12906
  "validate_build_plan",
12459
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}.",
12460
12908
  {
12461
- 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.")
12462
12910
  },
12463
12911
  async ({ plan }) => validateBuildPlan(plan)
12464
12912
  );
@@ -12466,12 +12914,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12466
12914
  "apply_build_plan",
12467
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.`,
12468
12916
  {
12469
- plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
12470
- 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)."),
12471
- locationId: import_zod53.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
12472
- 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.'),
12473
- publishWorkflows: import_zod53.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
12474
- 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.")
12475
12923
  },
12476
12924
  async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
12477
12925
  try {
@@ -12539,6 +12987,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12539
12987
  }
12540
12988
  const deps = makeExecuteDeps(client, builderClient, activeLocation);
12541
12989
  const exec = await executeBackbone(typedPlan, deps);
12990
+ const externalWiring = buildExternalWiring(typedPlan, exec.idMap, activeLocation);
12542
12991
  const execManualLines = exec.manual.map((m) => `[${m.type}] ${m.reason}`);
12543
12992
  const manualLines = result.workflows.flatMap((w) => [
12544
12993
  ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
@@ -12556,6 +13005,8 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12556
13005
  built: exec.built,
12557
13006
  manual: exec.manual,
12558
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,
12559
13010
  deferred: exec.deferred,
12560
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.',
12561
13012
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
@@ -12651,7 +13102,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12651
13102
  resolved.set(q.key, rec);
12652
13103
  }
12653
13104
  if (!missing) break;
12654
- if (attempt < 6) await sleep2(700 * attempt);
13105
+ if (attempt < 6) await sleep3(700 * attempt);
12655
13106
  }
12656
13107
  if (missing) {
12657
13108
  throw new Error(
@@ -12665,9 +13116,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12665
13116
  "install_intake_form",
12666
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.",
12667
13118
  {
12668
- dryRun: import_zod53.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
12669
- formId: import_zod53.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
12670
- 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}".`)
12671
13122
  },
12672
13123
  async ({ dryRun, formId, formName }) => {
12673
13124
  try {
@@ -12717,7 +13168,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12717
13168
  break;
12718
13169
  } catch (saveErr) {
12719
13170
  if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
12720
- await sleep2(700 * attempt);
13171
+ await sleep3(700 * attempt);
12721
13172
  continue;
12722
13173
  }
12723
13174
  throw saveErr;
@@ -12729,7 +13180,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12729
13180
  const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
12730
13181
  persistedCount = countFormFields(verify);
12731
13182
  if (persistedCount > 0) break;
12732
- if (attempt < 6) await sleep2(700 * attempt);
13183
+ if (attempt < 6) await sleep3(700 * attempt);
12733
13184
  }
12734
13185
  const fieldMap = {};
12735
13186
  for (const [key, rec] of resolved) fieldMap[key] = rec.id;
@@ -12755,10 +13206,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12755
13206
  "normalize_submission_to_brief",
12756
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).',
12757
13208
  {
12758
- formId: import_zod53.z.string().describe("The intake form ID (from install_intake_form)."),
12759
- submissionId: import_zod53.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
12760
- fieldMap: import_zod53.z.record(import_zod53.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
12761
- 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.")
12762
13213
  },
12763
13214
  async ({ formId, submissionId, fieldMap, preset }) => {
12764
13215
  try {
@@ -12787,7 +13238,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12787
13238
  const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
12788
13239
  resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
12789
13240
  }
12790
- 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();
12791
13242
  const presetParsed = presetSchema2.safeParse(preset);
12792
13243
  const brief = normalizeSubmissionToBrief({
12793
13244
  others,
@@ -12861,11 +13312,13 @@ var LOCATION_SWITCHER_MODULE = "location-switcher";
12861
13312
  var SNAPSHOTS_MODULE = "snapshots";
12862
13313
  var FORM_BUILDER_MODULE = "form-builder";
12863
13314
  var INTAKE_TO_BUILD_MODULE = "intake-to-build";
13315
+ var FUNNEL_QA_MODULE = "funnel-qa";
12864
13316
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
12865
13317
  ...publicApiTools.map(([, label]) => label),
12866
13318
  ...internalApiTools.map(([, label]) => label),
12867
13319
  FORM_BUILDER_MODULE,
12868
13320
  INTAKE_TO_BUILD_MODULE,
13321
+ FUNNEL_QA_MODULE,
12869
13322
  VALIDATORS_MODULE,
12870
13323
  DIAGNOSTICS_MODULE,
12871
13324
  LOCATION_SWITCHER_MODULE,
@@ -12885,6 +13338,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
12885
13338
  }
12886
13339
  registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
12887
13340
  registerIntakeToBuildTools(wrap(INTAKE_TO_BUILD_MODULE), client, builderClient);
13341
+ registerFunnelQaTools(wrap(FUNNEL_QA_MODULE), client, builderClient);
12888
13342
  registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
12889
13343
  registerDiagnosticTools(
12890
13344
  wrap(DIAGNOSTICS_MODULE),