@elitedcs/ghl-mcp 3.66.1 → 3.67.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
@@ -147,10 +147,10 @@ var init_ghl_client = __esm({
147
147
  throw new Error(`Request timeout (30s): ${method} ${path15}`);
148
148
  }
149
149
  if (!options.noRetry && attempt < MAX_RETRIES) {
150
- const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
151
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
150
+ const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
151
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
152
152
  `);
153
- await new Promise((r) => setTimeout(r, delay4));
153
+ await new Promise((r) => setTimeout(r, delay5));
154
154
  return this.request(method, path15, options, attempt + 1);
155
155
  }
156
156
  throw error;
@@ -158,10 +158,10 @@ var init_ghl_client = __esm({
158
158
  clearTimeout(timeout);
159
159
  }
160
160
  if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
161
- const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
162
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
161
+ const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
162
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
163
163
  `);
164
- await new Promise((r) => setTimeout(r, delay4));
164
+ await new Promise((r) => setTimeout(r, delay5));
165
165
  return this.request(method, path15, options, attempt + 1);
166
166
  }
167
167
  if (!response.ok) {
@@ -1648,7 +1648,7 @@ Note: Firebase credentials rejected (${fb.error}).`;
1648
1648
  const telemetryLine = telemetryDisabled(process.env) ? "" : `
1649
1649
 
1650
1650
  ${TELEMETRY_DISCLOSURE}`;
1651
- const finishedCount = isFree ? "111" : "239";
1651
+ const finishedCount = isFree ? "111" : "241";
1652
1652
  const freeTip = isFree ? `
1653
1653
 
1654
1654
  Free tier: read-only. Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate) upgrades in place \u2014 same install, you only swap the license key: https://ghlcommand.com` : "";
@@ -1693,7 +1693,7 @@ Free tier: read-only. Write tools stay visible but answer with upgrade info inst
1693
1693
  function registerEnableWorkflowBuilderTool(server2) {
1694
1694
  server2.tool(
1695
1695
  "enable_workflow_builder",
1696
- "Add Firebase credentials to an existing GHL Command install to unlock 55 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 184 to 239 after the next Claude restart.",
1696
+ "Add Firebase credentials to an existing GHL Command install to unlock 55 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 186 to 241 after the next Claude restart.",
1697
1697
  {
1698
1698
  // v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
1699
1699
  // get the console script; the script returns a JSON object that pastes
@@ -2854,9 +2854,9 @@ var require_package = __commonJS({
2854
2854
  "package.json"(exports2, module2) {
2855
2855
  module2.exports = {
2856
2856
  name: "@elitedcs/ghl-mcp",
2857
- version: "3.66.1",
2857
+ version: "3.67.0",
2858
2858
  mcpName: "io.github.drjerryrelth/ghl-command",
2859
- description: "GoHighLevel MCP Server for Claude. 239 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.",
2859
+ description: "GoHighLevel MCP Server for Claude. 241 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.",
2860
2860
  main: "dist/index.js",
2861
2861
  bin: {
2862
2862
  "ghl-mcp": "dist/index.js"
@@ -6187,10 +6187,10 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
6187
6187
  throw new Error(`Request timeout (30s): ${method} ${urlPath}`);
6188
6188
  }
6189
6189
  if (attempt < MAX_RETRIES2) {
6190
- const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS2);
6191
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${delay4}ms
6190
+ const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS2);
6191
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${delay5}ms
6192
6192
  `);
6193
- await new Promise((r) => setTimeout(r, delay4));
6193
+ await new Promise((r) => setTimeout(r, delay5));
6194
6194
  return this.request(method, urlPath, body, attempt + 1);
6195
6195
  }
6196
6196
  throw error;
@@ -6198,10 +6198,10 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
6198
6198
  clearTimeout(timeout);
6199
6199
  }
6200
6200
  if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
6201
- const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS2);
6202
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${delay4}ms
6201
+ const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS2);
6202
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${delay5}ms
6203
6203
  `);
6204
- await new Promise((r) => setTimeout(r, delay4));
6204
+ await new Promise((r) => setTimeout(r, delay5));
6205
6205
  return this.request(method, urlPath, body, attempt + 1);
6206
6206
  }
6207
6207
  if (!response.ok) {
@@ -10182,6 +10182,18 @@ function summarizeWebhookWorkflow(workflow) {
10182
10182
  };
10183
10183
  }
10184
10184
  var WorkflowListSchema = import_zod34.z.object({ rows: import_zod34.z.array(import_zod34.z.object({ _id: import_zod34.z.string().optional(), id: import_zod34.z.string().optional() }).passthrough()).optional() }).passthrough();
10185
+ async function loadWebhooks(client, limit) {
10186
+ const listed = WorkflowListSchema.parse(await client.listWorkflows(limit, 0));
10187
+ const rows = listed.rows ?? [];
10188
+ const webhooks = [];
10189
+ for (const row of rows) {
10190
+ const id = row._id ?? row.id;
10191
+ if (!id) continue;
10192
+ const summary = summarizeWebhookWorkflow(await client.getWorkflow(id));
10193
+ if (summary) webhooks.push(summary);
10194
+ }
10195
+ return { webhooks, scanned: rows.length };
10196
+ }
10185
10197
  function registerWebhookTools(server2, builderClient) {
10186
10198
  const requireClient = () => {
10187
10199
  if (!builderClient) {
@@ -10191,18 +10203,6 @@ function registerWebhookTools(server2, builderClient) {
10191
10203
  }
10192
10204
  return builderClient;
10193
10205
  };
10194
- const loadWebhooks = async (client, limit) => {
10195
- const listed = WorkflowListSchema.parse(await client.listWorkflows(limit, 0));
10196
- const rows = listed.rows ?? [];
10197
- const webhooks = [];
10198
- for (const row of rows) {
10199
- const id = row._id ?? row.id;
10200
- if (!id) continue;
10201
- const summary = summarizeWebhookWorkflow(await client.getWorkflow(id));
10202
- if (summary) webhooks.push(summary);
10203
- }
10204
- return { webhooks, scanned: rows.length };
10205
- };
10206
10206
  server2.tool(
10207
10207
  "list_webhooks",
10208
10208
  "List every webhook in the current sub-account. A webhook here is a workflow that fires a `webhook` action, so this also finds ones built by hand in the GHL UI. Returns the URL, HTTP method and triggering events for each. NOTE: GoHighLevel has no webhook REST API (it is Marketplace-OAuth only) \u2014 webhooks live on the workflow layer, which is why each one has a workflow ID and shows up in the client's Automation list. " + WEBHOOK_PAYLOAD_NOTE,
@@ -13688,11 +13688,407 @@ function registerBulkOperationTools(server2, client) {
13688
13688
 
13689
13689
  // src/tools/account-export.ts
13690
13690
  var import_zod46 = require("zod");
13691
+
13692
+ // src/contact-export.ts
13693
+ var CONTACT_PAGE_SIZE = 100;
13694
+ var DEFAULT_MAX_CONTACTS = 5e3;
13695
+ var MAX_PAGES = 1e3;
13696
+ function isComplete(reason) {
13697
+ return reason === "complete";
13698
+ }
13699
+ function stopReasonSentence(reason, fetched, cap) {
13700
+ switch (reason) {
13701
+ case "complete":
13702
+ return `All ${fetched} contacts were exported.`;
13703
+ case "cap-reached":
13704
+ return `INCOMPLETE: ${fetched} contacts were exported and the limit of ${cap} was reached. This account has more contacts than that. Run it again with a higher limit before treating this file as the full list.`;
13705
+ case "cursor-exhausted":
13706
+ return `INCOMPLETE: ${fetched} contacts were exported, then GoHighLevel stopped providing a way to ask for the next page. Treat this file as partial.`;
13707
+ case "cursor-stalled":
13708
+ return `INCOMPLETE: ${fetched} contacts were exported, then GoHighLevel began returning the same page repeatedly. The export stopped rather than loop. Treat this file as partial.`;
13709
+ case "page-ceiling":
13710
+ return `INCOMPLETE: ${fetched} contacts were exported and the internal page ceiling was reached. Treat this file as partial.`;
13711
+ }
13712
+ }
13691
13713
  function delay2(ms) {
13692
13714
  return new Promise((resolve8) => setTimeout(resolve8, ms));
13693
13715
  }
13694
- function registerAccountExportTools(server2, client, registry2) {
13695
- const builderClient = WorkflowBuilderClient.fromEnv();
13716
+ async function fetchAllContacts(client, locationId2, options = {}) {
13717
+ const cap = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
13718
+ const pauseMs = options.pauseMs ?? 100;
13719
+ const rows = [];
13720
+ let startAfter;
13721
+ let startAfterId;
13722
+ let pages = 0;
13723
+ let indexTotal = null;
13724
+ let stopReason = "complete";
13725
+ while (true) {
13726
+ if (pages >= MAX_PAGES) {
13727
+ stopReason = "page-ceiling";
13728
+ break;
13729
+ }
13730
+ const raw = await client.get("/contacts/", {
13731
+ params: {
13732
+ locationId: locationId2,
13733
+ limit: CONTACT_PAGE_SIZE,
13734
+ // Both cursor halves or neither. Sending one alone re-serves page 1.
13735
+ startAfter,
13736
+ startAfterId
13737
+ }
13738
+ });
13739
+ const parsed = ContactSearchResponseSchema.parse(raw);
13740
+ pages += 1;
13741
+ if (pages === 1 && typeof parsed.meta?.total === "number") {
13742
+ indexTotal = parsed.meta.total;
13743
+ }
13744
+ if (parsed.contacts.length === 0) {
13745
+ stopReason = "complete";
13746
+ break;
13747
+ }
13748
+ for (const contact of parsed.contacts) {
13749
+ rows.push(contact);
13750
+ if (rows.length >= cap) break;
13751
+ }
13752
+ if (rows.length >= cap) {
13753
+ stopReason = "cap-reached";
13754
+ break;
13755
+ }
13756
+ const nextAfter = parsed.meta?.startAfter ?? void 0;
13757
+ const nextAfterId = parsed.meta?.startAfterId ?? void 0;
13758
+ if (nextAfter == null || !nextAfterId) {
13759
+ stopReason = "cursor-exhausted";
13760
+ break;
13761
+ }
13762
+ if (nextAfter === startAfter && nextAfterId === startAfterId) {
13763
+ stopReason = "cursor-stalled";
13764
+ break;
13765
+ }
13766
+ startAfter = nextAfter;
13767
+ startAfterId = nextAfterId;
13768
+ if (pauseMs > 0) await delay2(pauseMs);
13769
+ }
13770
+ return {
13771
+ rows,
13772
+ fetched: rows.length,
13773
+ pages,
13774
+ stopReason,
13775
+ complete: isComplete(stopReason),
13776
+ indexTotal
13777
+ };
13778
+ }
13779
+ var PREFERRED_COLUMNS = [
13780
+ "id",
13781
+ "firstName",
13782
+ "lastName",
13783
+ "name",
13784
+ "email",
13785
+ "phone",
13786
+ "companyName",
13787
+ "tags",
13788
+ "source",
13789
+ "type",
13790
+ "dateAdded",
13791
+ "dateUpdated",
13792
+ "address1",
13793
+ "city",
13794
+ "state",
13795
+ "postalCode",
13796
+ "country",
13797
+ "timezone",
13798
+ "dnd"
13799
+ ];
13800
+ function neutralizeFormula(text) {
13801
+ return /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
13802
+ }
13803
+ function cell(value) {
13804
+ if (value === null || value === void 0) return "";
13805
+ const raw = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value);
13806
+ const text = neutralizeFormula(raw);
13807
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
13808
+ }
13809
+ function contactsToCsv(rows) {
13810
+ if (rows.length === 0) return "";
13811
+ const seen = /* @__PURE__ */ new Set();
13812
+ for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
13813
+ const preferred = PREFERRED_COLUMNS.filter((c) => seen.has(c));
13814
+ const rest = [...seen].filter((c) => !preferred.includes(c)).sort();
13815
+ const columns = [...preferred, ...rest];
13816
+ const lines = [columns.join(",")];
13817
+ for (const row of rows) lines.push(columns.map((c) => cell(row[c])).join(","));
13818
+ return lines.join("\n");
13819
+ }
13820
+
13821
+ // src/asset-ownership.ts
13822
+ var OWNERSHIP_PREFIX = "Offboarding Owner: ";
13823
+ var OWNER_KINDS = ["client", "agency", "borrowed", "third-party"];
13824
+ var OWNER_MEANING = {
13825
+ client: "The client owns this. It stays with them and they keep paying for it.",
13826
+ agency: "The agency owns this. It leaves with the agency unless it is sold or transferred.",
13827
+ borrowed: "Borrowed for the engagement. It must be returned, disconnected, or replaced at exit.",
13828
+ "third-party": "Owned by an outside vendor. Transfer depends on that vendor's own process."
13829
+ };
13830
+ function readCustomValues(raw) {
13831
+ const box = raw && typeof raw === "object" ? raw : {};
13832
+ const list = Array.isArray(box.customValues) ? box.customValues : [];
13833
+ return list.flatMap((entry) => {
13834
+ if (!entry || typeof entry !== "object") return [];
13835
+ const row = entry;
13836
+ if (typeof row.id !== "string" || typeof row.name !== "string") return [];
13837
+ return [{ id: row.id, name: row.name, value: typeof row.value === "string" ? row.value : "" }];
13838
+ });
13839
+ }
13840
+ function isOwnerKind(value) {
13841
+ return OWNER_KINDS.includes(value);
13842
+ }
13843
+ function encodeOwnership(owner, note) {
13844
+ return note && note.trim() ? `${owner} | ${note.trim()}` : owner;
13845
+ }
13846
+ function decodeOwnership(value) {
13847
+ const [head, ...rest] = value.split("|");
13848
+ const owner = head.trim();
13849
+ const note = rest.join("|").trim();
13850
+ return { owner: isOwnerKind(owner) ? owner : null, note: note || void 0 };
13851
+ }
13852
+ async function readOwnership(client, locationId2) {
13853
+ const values = readCustomValues(await client.get(`/locations/${locationId2}/customValues`));
13854
+ const records = [];
13855
+ const unreadable = [];
13856
+ for (const value of values) {
13857
+ if (!value.name.startsWith(OWNERSHIP_PREFIX)) continue;
13858
+ const asset = value.name.slice(OWNERSHIP_PREFIX.length).trim();
13859
+ const { owner, note } = decodeOwnership(value.value);
13860
+ if (!owner) {
13861
+ unreadable.push(`${asset}: "${value.value}"`);
13862
+ continue;
13863
+ }
13864
+ records.push({ asset, owner, note, customValueId: value.id });
13865
+ }
13866
+ return { records, unreadable };
13867
+ }
13868
+ async function writeOwnership(client, locationId2, entries) {
13869
+ const existing = readCustomValues(await client.get(`/locations/${locationId2}/customValues`));
13870
+ const results = [];
13871
+ for (const entry of entries) {
13872
+ const asset = entry.asset.trim();
13873
+ const name = `${OWNERSHIP_PREFIX}${asset}`;
13874
+ const matches = existing.filter((value) => value.name === name);
13875
+ if (matches.length > 1) {
13876
+ results.push({
13877
+ asset,
13878
+ action: "halted",
13879
+ reason: `${matches.length} custom values are named "${name}". Updating by name cannot tell them apart, so nothing was changed. Delete the extras in GHL, then run this again.`
13880
+ });
13881
+ continue;
13882
+ }
13883
+ const body = { name, value: encodeOwnership(entry.owner, entry.note) };
13884
+ if (matches.length === 1) {
13885
+ if (!matches[0].id) {
13886
+ results.push({
13887
+ asset,
13888
+ action: "halted",
13889
+ reason: `"${name}" was created earlier in this request but GoHighLevel did not return its id, so it cannot be updated safely. Run this asset again on its own.`
13890
+ });
13891
+ continue;
13892
+ }
13893
+ await client.put(`/locations/${locationId2}/customValues/${matches[0].id}`, { body });
13894
+ results.push({ asset, action: "updated", customValueId: matches[0].id });
13895
+ continue;
13896
+ }
13897
+ const created = await client.post(`/locations/${locationId2}/customValues`, { body });
13898
+ const createdId = readCreatedId(created);
13899
+ existing.push({ id: createdId ?? "", name, value: body.value });
13900
+ results.push({ asset, action: "created", customValueId: createdId });
13901
+ }
13902
+ return results;
13903
+ }
13904
+ function readCreatedId(raw) {
13905
+ if (!raw || typeof raw !== "object") return void 0;
13906
+ const box = raw;
13907
+ if (typeof box.id === "string") return box.id;
13908
+ const nested = box.customValue;
13909
+ if (nested && typeof nested === "object") {
13910
+ const inner = nested.id;
13911
+ if (typeof inner === "string") return inner;
13912
+ }
13913
+ return void 0;
13914
+ }
13915
+
13916
+ // src/handoff-doc.ts
13917
+ function sectionItems(section2) {
13918
+ return Array.isArray(section2) ? section2 : [];
13919
+ }
13920
+ function bullet(lines) {
13921
+ return lines.length ? lines.map((l) => `- ${l}`).join("\n") : "- None found.";
13922
+ }
13923
+ function plainReason(reason) {
13924
+ const withoutBody = reason.split(/[{\n]/)[0];
13925
+ const withoutRoute = withoutBody.replace(/:?\s*(GET|POST|PUT|DELETE)\s+\/\S*/g, "");
13926
+ const cleaned = withoutRoute.replace(/\s+/g, " ").replace(/[:\s]+$/, "").trim();
13927
+ return cleaned || "the account did not return this section";
13928
+ }
13929
+ function inventoryTable(inventory) {
13930
+ const rows = Object.entries(inventory).map(([section2, data]) => {
13931
+ if (data.unavailable) return `| ${section2} | not read | ${plainReason(data.unavailable)} |`;
13932
+ const sample = data.names.slice(0, 8).join(", ");
13933
+ const more = data.names.length > 8 ? `, and ${data.names.length - 8} more` : "";
13934
+ return `| ${section2} | ${data.count ?? data.names.length} | ${sample || "none"}${more} |`;
13935
+ });
13936
+ return ["| What | How many | Which ones |", "| --- | --- | --- |", ...rows].join("\n");
13937
+ }
13938
+ function sectionList(section2, render) {
13939
+ if (!Array.isArray(section2)) {
13940
+ return `**Not read.** ${plainReason(section2.unavailable)}. This section is unknown, which is NOT the same as empty. Check it by hand before you rely on this document.`;
13941
+ }
13942
+ return bullet(section2.map(render));
13943
+ }
13944
+ function buildHandoffDoc(input) {
13945
+ const {
13946
+ accountName,
13947
+ locationId: locationId2,
13948
+ generatedAt,
13949
+ contacts,
13950
+ inventory,
13951
+ users,
13952
+ phoneNumbers,
13953
+ socialAccounts,
13954
+ webhooks,
13955
+ webhookScanPartial,
13956
+ ownership
13957
+ } = input;
13958
+ const ownedBy = (kind) => ownership.recorded.filter((r) => r.owner === kind);
13959
+ const ownershipSection = ownership.recorded.length ? ["client", "agency", "borrowed", "third-party"].map((kind) => {
13960
+ const items = ownedBy(kind);
13961
+ if (!items.length) return "";
13962
+ return `**${kind}** ${OWNER_MEANING[kind]}
13963
+
13964
+ ${bullet(
13965
+ items.map((i) => i.note ? `${i.asset} (${i.note})` : i.asset)
13966
+ )}`;
13967
+ }).filter(Boolean).join("\n\n") : "Nothing has been recorded yet. Every asset below is unanswered.";
13968
+ const unknownSection = ownership.unknown.length ? `These exist in the account but nobody has said who owns them. Answer them once and they stay answered:
13969
+
13970
+ ${bullet(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
13971
+ const webhookSection = sectionList(webhooks, (w) => `${w.name} sends to ${w.url}${w.events.length ? ` when: ${w.events.join(", ")}` : ""}`) + (webhookScanPartial ? `
13972
+
13973
+ **This list may be incomplete.** ${webhookScanPartial}` : "");
13974
+ return `# Account handoff: ${accountName}
13975
+
13976
+ Prepared ${generatedAt}. GoHighLevel account ID ${locationId2}.
13977
+
13978
+ This document describes what is inside this account, who can get into it, and
13979
+ what has to change hands. It was generated from the live account, not from
13980
+ memory.
13981
+
13982
+ ## Read this first
13983
+
13984
+ ${contacts.complete ? "" : '**The contact export in this package is incomplete. See "Contacts" below before you rely on it.**\n\n'}Three things this document deliberately does not contain:
13985
+
13986
+ 1. **No passwords or API keys.** GoHighLevel does not hand out key values to any
13987
+ tool, including this one. What you get instead is a list of every place a key
13988
+ exists, so you can rotate them. That list is at the end.
13989
+ 2. **No opinion about who owns what**, beyond what somebody actually recorded.
13990
+ Anything unanswered is listed as unanswered.
13991
+ 3. **No guesses about what is missing.** If a section could not be read, it says
13992
+ so and why, rather than showing zero.
13993
+
13994
+ ## Contacts
13995
+
13996
+ ${contacts.sentence}${contacts.indexTotal !== null ? `
13997
+
13998
+ GoHighLevel's own index reports **${contacts.indexTotal}** contacts for this account. This export contains **${contacts.exported}**. These two numbers are counted differently and GoHighLevel's index updates on a delay, so a small difference is normal and does not by itself mean the export is short.` : ""}
13999
+
14000
+ ## What is in the account
14001
+
14002
+ ${inventoryTable(inventory)}
14003
+
14004
+ ## Who can get in
14005
+
14006
+ ### People with logins
14007
+
14008
+ ${sectionList(users, (u) => `${u.name} (${u.email})${u.roles ? `, ${u.roles}` : ""}`)}
14009
+
14010
+ Every person above can log into this account right now. Remove the ones who
14011
+ should not be able to, on the day the relationship ends, not later.
14012
+
14013
+ ### Phone numbers
14014
+
14015
+ ${sectionList(phoneNumbers, (p) => p.label ? `${p.number} (${p.label})` : p.number)}
14016
+
14017
+ Numbers are rented from GoHighLevel and stop working if the account lapses. If
14018
+ the client needs to keep a number, it has to be ported out before then, and
14019
+ porting takes days to weeks.
14020
+
14021
+ ### Connected accounts
14022
+
14023
+ ${sectionList(socialAccounts, (s) => `${s.platform}: ${s.name}`)}
14024
+
14025
+ These connections were authorised by a person. If that person leaves, the
14026
+ connection can break even though nothing in GoHighLevel changed.
14027
+
14028
+ ### Outbound webhooks
14029
+
14030
+ ${webhookSection}
14031
+
14032
+ Each of these sends data out of this account to somewhere else. Confirm the
14033
+ receiving system should still be receiving it.
14034
+
14035
+ ## Who owns what
14036
+
14037
+ ${ownershipSection}
14038
+
14039
+ ### Not yet answered
14040
+
14041
+ ${unknownSection}
14042
+ ${ownership.unreadable.length ? `
14043
+ ### Recorded but unreadable
14044
+
14045
+ Somebody edited these by hand and the answer no longer parses. They need a human:
14046
+
14047
+ ${bullet(ownership.unreadable)}
14048
+ ` : ""}
14049
+ ## Rotate these on the way out
14050
+
14051
+ Key values cannot be read by any tool, so this is a checklist of places to go
14052
+ and change something, not a list of secrets.
14053
+
14054
+ - The GoHighLevel Private Integration key for this account. Anyone holding the
14055
+ old one keeps full access until it is deleted.
14056
+ - Any Firebase or agency-level credential used to reach this account.
14057
+ - Every connected account listed above, if the departing party authorised it.
14058
+ - Every webhook destination above, if it authenticates with a shared secret.
14059
+ - Any payment processor, email sending domain, or advertising account attached
14060
+ to this business but living outside GoHighLevel.
14061
+
14062
+ ## What to do next
14063
+
14064
+ 1. Save the contact export alongside this document.
14065
+ 2. Answer anything under "Not yet answered" and re-run the kit so the answers
14066
+ are stored in the account itself.
14067
+ 3. Work down the rotation list above.
14068
+ 4. Remove the logins that should no longer exist.
14069
+ `;
14070
+ }
14071
+
14072
+ // src/tools/account-export.ts
14073
+ function delay3(ms) {
14074
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
14075
+ }
14076
+ function builderSectionAvailability(builderLocationId, requestedLocationId) {
14077
+ if (!builderLocationId) {
14078
+ return {
14079
+ usable: false,
14080
+ reason: "Not read: the workflow builder is not connected. Run enable_workflow_builder to add Firebase credentials, then export again."
14081
+ };
14082
+ }
14083
+ if (builderLocationId !== requestedLocationId) {
14084
+ return {
14085
+ usable: false,
14086
+ reason: `Not read: the workflow builder is connected to a DIFFERENT sub-account (${builderLocationId}), not the one being exported (${requestedLocationId}). Reading it would describe the wrong account. Run switch_location to ${requestedLocationId} and export again.`
14087
+ };
14088
+ }
14089
+ return { usable: true };
14090
+ }
14091
+ function registerAccountExportTools(server2, client, registry2, builderClient) {
13696
14092
  server2.tool(
13697
14093
  "export_account",
13698
14094
  "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.",
@@ -13709,10 +14105,10 @@ function registerAccountExportTools(server2, client, registry2) {
13709
14105
  };
13710
14106
  try {
13711
14107
  report.location = await client.get(`/locations/${locId}`);
13712
- } catch {
13713
- report.location = "Error fetching";
14108
+ } catch (e) {
14109
+ report.location = `Error fetching: ${errorMessage(e)}`;
13714
14110
  }
13715
- await delay2(100);
14111
+ await delay3(100);
13716
14112
  try {
13717
14113
  const contacts = await client.get("/contacts/", {
13718
14114
  params: { locationId: locId, limit: includeContacts ? 100 : 1 }
@@ -13724,36 +14120,38 @@ function registerAccountExportTools(server2, client, registry2) {
13724
14120
  total: typeof meta.total === "number" ? meta.total : contactList.length,
13725
14121
  sample: includeContacts ? contactList : contactList.slice(0, 1)
13726
14122
  };
13727
- } catch {
13728
- report.contacts = "Error fetching";
14123
+ } catch (e) {
14124
+ report.contacts = `Error fetching: ${errorMessage(e)}`;
13729
14125
  }
13730
- await delay2(100);
14126
+ await delay3(100);
13731
14127
  try {
13732
14128
  report.pipelines = await client.get("/opportunities/pipelines", {
13733
14129
  params: { locationId: locId }
13734
14130
  });
13735
- } catch {
13736
- report.pipelines = "Error fetching";
14131
+ } catch (e) {
14132
+ report.pipelines = `Error fetching: ${errorMessage(e)}`;
13737
14133
  }
13738
- await delay2(100);
14134
+ await delay3(100);
13739
14135
  try {
13740
14136
  report.workflows = await client.get("/workflows/", {
13741
14137
  params: { locationId: locId }
13742
14138
  });
13743
- } catch {
13744
- report.workflows = "Error fetching";
14139
+ } catch (e) {
14140
+ report.workflows = `Error fetching: ${errorMessage(e)}`;
13745
14141
  }
13746
- await delay2(100);
13747
- if (builderClient) {
14142
+ await delay3(100);
14143
+ const builderUse = builderSectionAvailability(builderClient?.locationId, locId);
14144
+ if (builderUse.usable && builderClient) {
13748
14145
  try {
13749
- const fullWorkflows = await builderClient.listWorkflows(50, 0);
13750
- report.workflowsFull = fullWorkflows;
13751
- } catch {
13752
- report.workflowsFull = "Error fetching (builder auth may have expired)";
14146
+ report.workflowsFull = await builderClient.listWorkflows(50, 0);
14147
+ } catch (e) {
14148
+ report.workflowsFull = `Error fetching: ${errorMessage(e)}`;
13753
14149
  }
13754
- await delay2(100);
14150
+ await delay3(100);
14151
+ } else {
14152
+ report.workflowsFull = builderUse.reason;
13755
14153
  }
13756
- if (builderClient) {
14154
+ if (builderUse.usable && builderClient) {
13757
14155
  try {
13758
14156
  const headers = await builderClient.buildHeaders();
13759
14157
  const res = await fetch(
@@ -13761,60 +14159,60 @@ function registerAccountExportTools(server2, client, registry2) {
13761
14159
  { headers }
13762
14160
  );
13763
14161
  if (res.ok) report.funnels = await res.json();
13764
- else report.funnels = "Error fetching";
13765
- } catch {
13766
- report.funnels = "Error fetching";
14162
+ else report.funnels = `Error fetching: builder returned HTTP ${res.status}`;
14163
+ } catch (e) {
14164
+ report.funnels = `Error fetching: ${errorMessage(e)}`;
13767
14165
  }
13768
- await delay2(100);
14166
+ await delay3(100);
13769
14167
  } else {
13770
14168
  try {
13771
14169
  report.funnels = await client.get("/funnels/", {
13772
14170
  params: { locationId: locId }
13773
14171
  });
13774
- } catch {
13775
- report.funnels = "Error fetching";
14172
+ } catch (e) {
14173
+ report.funnels = `Error fetching: ${errorMessage(e)}`;
13776
14174
  }
13777
14175
  }
13778
14176
  try {
13779
14177
  report.forms = await client.get("/forms/", {
13780
14178
  params: { locationId: locId }
13781
14179
  });
13782
- } catch {
13783
- report.forms = "Error fetching";
14180
+ } catch (e) {
14181
+ report.forms = `Error fetching: ${errorMessage(e)}`;
13784
14182
  }
13785
- await delay2(100);
14183
+ await delay3(100);
13786
14184
  try {
13787
14185
  report.customFields = await client.get("/locations/" + locId + "/customFields");
13788
- } catch {
13789
- report.customFields = "Error fetching";
14186
+ } catch (e) {
14187
+ report.customFields = `Error fetching: ${errorMessage(e)}`;
13790
14188
  }
13791
- await delay2(100);
14189
+ await delay3(100);
13792
14190
  try {
13793
14191
  report.customValues = await client.get("/locations/" + locId + "/customValues");
13794
- } catch {
13795
- report.customValues = "Error fetching";
14192
+ } catch (e) {
14193
+ report.customValues = `Error fetching: ${errorMessage(e)}`;
13796
14194
  }
13797
- await delay2(100);
14195
+ await delay3(100);
13798
14196
  try {
13799
14197
  report.tags = await client.get("/locations/" + locId + "/tags");
13800
- } catch {
13801
- report.tags = "Error fetching";
14198
+ } catch (e) {
14199
+ report.tags = `Error fetching: ${errorMessage(e)}`;
13802
14200
  }
13803
- await delay2(100);
14201
+ await delay3(100);
13804
14202
  try {
13805
14203
  report.calendars = await client.get("/calendars/", {
13806
14204
  params: { locationId: locId }
13807
14205
  });
13808
- } catch {
13809
- report.calendars = "Error fetching";
14206
+ } catch (e) {
14207
+ report.calendars = `Error fetching: ${errorMessage(e)}`;
13810
14208
  }
13811
- await delay2(100);
14209
+ await delay3(100);
13812
14210
  try {
13813
14211
  report.users = await client.get("/users/", {
13814
14212
  params: { locationId: locId }
13815
14213
  });
13816
- } catch {
13817
- report.users = "Error fetching";
14214
+ } catch (e) {
14215
+ report.users = `Error fetching: ${errorMessage(e)}`;
13818
14216
  }
13819
14217
  return jsonResponse(report);
13820
14218
  } catch (error) {
@@ -13854,13 +14252,13 @@ function registerAccountExportTools(server2, client, registry2) {
13854
14252
  const fetchData = async (locId) => {
13855
14253
  const data = {};
13856
14254
  data.pipelines = await read(() => client.get("/opportunities/pipelines", { params: { locationId: locId } }));
13857
- await delay2(100);
14255
+ await delay3(100);
13858
14256
  data.workflows = await read(() => client.get("/workflows/", { params: { locationId: locId } }));
13859
- await delay2(100);
14257
+ await delay3(100);
13860
14258
  data.customFields = await read(() => client.get(`/locations/${locId}/customFields`));
13861
- await delay2(100);
14259
+ await delay3(100);
13862
14260
  data.tags = await read(() => client.get(`/locations/${locId}/tags`));
13863
- await delay2(100);
14261
+ await delay3(100);
13864
14262
  data.forms = await read(() => client.get("/forms/", { params: { locationId: locId } }));
13865
14263
  return data;
13866
14264
  };
@@ -13893,6 +14291,270 @@ function registerAccountExportTools(server2, client, registry2) {
13893
14291
  }
13894
14292
  }
13895
14293
  );
14294
+ server2.tool(
14295
+ "build_offboarding_kit",
14296
+ "Assemble the complete exit package for a sub-account: a full contact export, an inventory of everything the account contains, an access map of every person and connected account that can reach it, the recorded owner of each asset, a key-rotation checklist, and a plain-language handoff document the next operator can follow. Read-only: it changes nothing. locationId is REQUIRED and never defaults, because exporting the wrong client's account is the one mistake this tool must not be able to make. Assets nobody has recorded an owner for come back under ownership.unanswered \u2014 answer those with record_asset_ownership and they stay answered. Key VALUES are never included: GoHighLevel does not expose them to any tool, so the kit lists where each key lives instead.",
14297
+ {
14298
+ locationId: import_zod46.z.string().describe("Sub-account to build the kit for. REQUIRED \u2014 this tool never falls back to the active account."),
14299
+ contacts: import_zod46.z.enum(["summary", "csv"]).optional().describe(
14300
+ "'summary' (default) returns contact counts only. 'csv' also returns the full contact export as CSV text, which can be large \u2014 ask for it when you are ready to save the file."
14301
+ ),
14302
+ maxContacts: import_zod46.z.number().int().positive().optional().describe(`Ceiling on contacts read. Defaults to ${DEFAULT_MAX_CONTACTS}. If the ceiling is hit, the kit says so in the handoff document itself rather than quietly returning a short list.`)
14303
+ },
14304
+ async ({ locationId: locationId2, contacts, maxContacts }) => {
14305
+ const originalKey = client.getApiKey();
14306
+ const registered = registry2?.getToken(locationId2);
14307
+ const usedRegisteredKey = Boolean(registered?.apiKey && registered.apiKey !== originalKey);
14308
+ if (usedRegisteredKey && registered?.apiKey) client.setApiKey(registered.apiKey);
14309
+ try {
14310
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
14311
+ let accountName = locationId2;
14312
+ let location = null;
14313
+ try {
14314
+ location = await client.get(`/locations/${locationId2}`);
14315
+ const box = location && typeof location === "object" ? location : {};
14316
+ const inner = box.location && typeof box.location === "object" ? box.location : box;
14317
+ if (typeof inner.name === "string" && inner.name) accountName = inner.name;
14318
+ } catch (e) {
14319
+ location = `Error fetching: ${errorMessage(e)}`;
14320
+ }
14321
+ await delay3(100);
14322
+ const inventory = {};
14323
+ const section2 = async (label, fn) => {
14324
+ try {
14325
+ const names = extractNames(await fn());
14326
+ inventory[label] = { count: names.length, names };
14327
+ } catch (e) {
14328
+ inventory[label] = { count: null, names: [], unavailable: errorMessage(e) };
14329
+ }
14330
+ await delay3(100);
14331
+ };
14332
+ await section2("pipelines", () => client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }));
14333
+ await section2("workflows", () => client.get("/workflows/", { params: { locationId: locationId2 } }));
14334
+ await section2("funnels", () => client.get("/funnels/", { params: { locationId: locationId2 } }));
14335
+ await section2("forms", () => client.get("/forms/", { params: { locationId: locationId2 } }));
14336
+ await section2("calendars", () => client.get("/calendars/", { params: { locationId: locationId2 } }));
14337
+ await section2("customFields", () => client.get(`/locations/${locationId2}/customFields`));
14338
+ await section2("customValues", () => client.get(`/locations/${locationId2}/customValues`));
14339
+ await section2("tags", () => client.get(`/locations/${locationId2}/tags`));
14340
+ const users = await readUsers(client, locationId2);
14341
+ await delay3(100);
14342
+ const phoneNumbers = await readPhoneNumbers(client, locationId2);
14343
+ await delay3(100);
14344
+ const socialAccounts = await readSocialAccounts(client, locationId2);
14345
+ await delay3(100);
14346
+ const WEBHOOK_SCAN_LIMIT = 50;
14347
+ const builderUse = builderSectionAvailability(builderClient?.locationId, locationId2);
14348
+ let webhooks;
14349
+ let webhookScanPartial;
14350
+ if (builderUse.usable && builderClient) {
14351
+ try {
14352
+ const scan = await loadWebhooks(builderClient, WEBHOOK_SCAN_LIMIT);
14353
+ const stillOurs = builderSectionAvailability(builderClient.locationId, locationId2);
14354
+ if (!stillOurs.usable) {
14355
+ webhooks = { unavailable: `The active account changed while webhooks were being read, so the result was discarded. ${stillOurs.reason ?? ""}` };
14356
+ } else {
14357
+ webhooks = scan.webhooks.map((w) => ({
14358
+ name: w.name,
14359
+ url: w.actions[0]?.url ?? "",
14360
+ events: w.events
14361
+ }));
14362
+ if (scan.scanned >= WEBHOOK_SCAN_LIMIT) {
14363
+ webhookScanPartial = `Only the first ${WEBHOOK_SCAN_LIMIT} workflows were checked and this account has at least that many, so a webhook built into a later workflow would not appear here. Check the account's Automation list by hand.`;
14364
+ }
14365
+ }
14366
+ } catch (e) {
14367
+ webhooks = { unavailable: `Error reading webhooks: ${errorMessage(e)}` };
14368
+ }
14369
+ } else {
14370
+ webhooks = { unavailable: builderUse.reason ?? "Not read." };
14371
+ }
14372
+ let ownershipRecords = { records: [], unreadable: [] };
14373
+ let ownershipError;
14374
+ try {
14375
+ ownershipRecords = await readOwnership(client, locationId2);
14376
+ } catch (e) {
14377
+ ownershipError = errorMessage(e);
14378
+ }
14379
+ const discovered = discoverAssets({ phoneNumbers, socialAccounts, inventory });
14380
+ const answered = new Set(ownershipRecords.records.map((r) => r.asset.toLowerCase()));
14381
+ const unanswered = discovered.filter((asset) => !answered.has(asset.toLowerCase()));
14382
+ const cap = maxContacts ?? DEFAULT_MAX_CONTACTS;
14383
+ let contactResult;
14384
+ let contactSentence;
14385
+ try {
14386
+ contactResult = await fetchAllContacts(client, locationId2, { maxContacts: cap });
14387
+ contactSentence = stopReasonSentence(contactResult.stopReason, contactResult.fetched, cap);
14388
+ } catch (e) {
14389
+ contactResult = { rows: [], fetched: 0, pages: 0, stopReason: "cursor-exhausted", complete: false, indexTotal: null };
14390
+ contactSentence = `INCOMPLETE: the contact export failed and returned nothing. Reason: ${errorMessage(e)}`;
14391
+ }
14392
+ const handoffDoc = buildHandoffDoc({
14393
+ accountName,
14394
+ locationId: locationId2,
14395
+ generatedAt,
14396
+ contacts: {
14397
+ exported: contactResult.fetched,
14398
+ indexTotal: contactResult.indexTotal,
14399
+ complete: contactResult.complete,
14400
+ sentence: contactSentence
14401
+ },
14402
+ inventory,
14403
+ users,
14404
+ phoneNumbers,
14405
+ socialAccounts,
14406
+ webhooks,
14407
+ webhookScanPartial,
14408
+ ownership: {
14409
+ recorded: ownershipRecords.records,
14410
+ unknown: unanswered,
14411
+ unreadable: ownershipRecords.unreadable
14412
+ }
14413
+ });
14414
+ return jsonResponse({
14415
+ account: { locationId: locationId2, name: accountName, generatedAt, usedRegisteredKey },
14416
+ contacts: {
14417
+ exported: contactResult.fetched,
14418
+ ghlIndexTotal: contactResult.indexTotal,
14419
+ complete: contactResult.complete,
14420
+ stopReason: contactResult.stopReason,
14421
+ pagesRead: contactResult.pages,
14422
+ statement: contactSentence,
14423
+ ...contacts === "csv" ? { csv: contactsToCsv(contactResult.rows) } : {
14424
+ csvNote: "Run again with contacts:'csv' to get the full export as CSV text to save."
14425
+ }
14426
+ },
14427
+ inventory,
14428
+ accessMap: {
14429
+ users,
14430
+ phoneNumbers,
14431
+ socialAccounts,
14432
+ webhooks,
14433
+ ...webhookScanPartial ? { webhookScanPartial } : {}
14434
+ },
14435
+ ownership: {
14436
+ recorded: ownershipRecords.records,
14437
+ unanswered,
14438
+ unreadable: ownershipRecords.unreadable,
14439
+ ...ownershipError ? { error: ownershipError } : {},
14440
+ howToAnswer: "Record these with record_asset_ownership so the next run needs no interview."
14441
+ },
14442
+ keyRotationNote: "Key VALUES are not retrievable from GoHighLevel by any tool. The handoff document lists where each key lives so they can be rotated.",
14443
+ handoffDoc
14444
+ });
14445
+ } catch (error) {
14446
+ return errorResponse(error);
14447
+ } finally {
14448
+ client.setApiKey(originalKey);
14449
+ }
14450
+ }
14451
+ );
14452
+ server2.tool(
14453
+ "record_asset_ownership",
14454
+ "Record who owns an asset attached to a sub-account: the client, the agency, something borrowed for the engagement, or an outside vendor. Stored as custom values inside the client's own account, so the record outlives this tool and the client can read it without asking anyone. Answer these once at setup and build_offboarding_kit needs no interview later. Re-recording the same asset updates it in place instead of adding a duplicate. locationId is REQUIRED: writing ownership into the wrong client's account is worth preventing outright.",
14455
+ {
14456
+ locationId: import_zod46.z.string().describe("Sub-account to record ownership in. REQUIRED \u2014 never falls back to the active account."),
14457
+ assets: import_zod46.z.array(
14458
+ import_zod46.z.object({
14459
+ asset: import_zod46.z.string().describe("What it is, in the words you would say out loud. e.g. 'Website domain' or 'Phone number +16025551234'."),
14460
+ owner: import_zod46.z.enum(OWNER_KINDS).describe("client = stays with them. agency = leaves with you. borrowed = must be returned or replaced at exit. third-party = an outside vendor controls it."),
14461
+ note: import_zod46.z.string().optional().describe("Anything the next person needs, e.g. 'registered on the client's own GoDaddy account'.")
14462
+ })
14463
+ ).min(1).describe("One entry per asset. Send several at once.")
14464
+ },
14465
+ async ({ locationId: locationId2, assets }) => {
14466
+ const originalKey = client.getApiKey();
14467
+ const registered = registry2?.getToken(locationId2);
14468
+ if (registered?.apiKey && registered.apiKey !== originalKey) client.setApiKey(registered.apiKey);
14469
+ try {
14470
+ const results = await writeOwnership(client, locationId2, assets);
14471
+ const halted = results.filter((r) => r.action === "halted");
14472
+ return jsonResponse({
14473
+ locationId: locationId2,
14474
+ recorded: results.filter((r) => r.action !== "halted"),
14475
+ ...halted.length ? { halted, warning: "Some assets were NOT recorded. See halted for which, and why." } : {}
14476
+ });
14477
+ } catch (error) {
14478
+ return errorResponse(error);
14479
+ } finally {
14480
+ client.setApiKey(originalKey);
14481
+ }
14482
+ }
14483
+ );
14484
+ }
14485
+ function extractNames(payload) {
14486
+ const list = Array.isArray(payload) ? payload : payload && typeof payload === "object" ? Object.values(payload).find(Array.isArray) : void 0;
14487
+ if (!Array.isArray(list)) return [];
14488
+ return list.map(
14489
+ (item) => item && typeof item === "object" ? String(item.name ?? item.id ?? "") : String(item)
14490
+ ).filter(Boolean);
14491
+ }
14492
+ async function readSection(read) {
14493
+ try {
14494
+ return await read();
14495
+ } catch (e) {
14496
+ return { unavailable: errorMessage(e) };
14497
+ }
14498
+ }
14499
+ async function readUsers(client, locationId2) {
14500
+ return readSection(async () => {
14501
+ const raw = await client.get("/users/", { params: { locationId: locationId2 } });
14502
+ const box = raw && typeof raw === "object" ? raw : {};
14503
+ const list = Array.isArray(box.users) ? box.users : [];
14504
+ return list.flatMap((entry) => {
14505
+ if (!entry || typeof entry !== "object") return [];
14506
+ const u = entry;
14507
+ const roles = u.roles && typeof u.roles === "object" ? String(u.roles.role ?? "") : "";
14508
+ return [{
14509
+ name: String(u.name ?? `${u.firstName ?? ""} ${u.lastName ?? ""}`).trim() || "(unnamed)",
14510
+ email: String(u.email ?? ""),
14511
+ roles
14512
+ }];
14513
+ });
14514
+ });
14515
+ }
14516
+ async function readPhoneNumbers(client, locationId2) {
14517
+ return readSection(async () => {
14518
+ const raw = await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } });
14519
+ const box = raw && typeof raw === "object" ? raw : {};
14520
+ const list = Array.isArray(box.phoneNumbers) ? box.phoneNumbers : [];
14521
+ return list.flatMap((entry) => {
14522
+ if (!entry || typeof entry !== "object") return [];
14523
+ const n = entry;
14524
+ const number = String(n.value ?? n.number ?? "");
14525
+ return number ? [{ number, label: n.title ? String(n.title) : void 0 }] : [];
14526
+ });
14527
+ });
14528
+ }
14529
+ async function readSocialAccounts(client, locationId2) {
14530
+ return readSection(async () => {
14531
+ const raw = await client.get(`/social-media-posting/${locationId2}/accounts`);
14532
+ const box = raw && typeof raw === "object" ? raw : {};
14533
+ const results = box.results && typeof box.results === "object" ? box.results : {};
14534
+ const list = Array.isArray(results.accounts) ? results.accounts : [];
14535
+ return list.flatMap((entry) => {
14536
+ if (!entry || typeof entry !== "object") return [];
14537
+ const a = entry;
14538
+ return [{ platform: String(a.platform ?? "unknown"), name: String(a.name ?? a.id ?? "") }];
14539
+ });
14540
+ });
14541
+ }
14542
+ var OFF_PLATFORM_ASSETS = [
14543
+ "Website domain",
14544
+ "Logo and brand files",
14545
+ "Meta pixel or ad account",
14546
+ "Google Analytics",
14547
+ "Payment processor account",
14548
+ "Email sending domain"
14549
+ ];
14550
+ function discoverAssets(input) {
14551
+ const assets = [];
14552
+ for (const p of sectionItems(input.phoneNumbers)) assets.push(`Phone number ${p.number}`);
14553
+ for (const s of sectionItems(input.socialAccounts)) assets.push(`${s.platform} account ${s.name}`.trim());
14554
+ const funnels = input.inventory.funnels;
14555
+ if (funnels && !funnels.unavailable) for (const name of funnels.names) assets.push(`Funnel "${name}"`);
14556
+ assets.push(...OFF_PLATFORM_ASSETS);
14557
+ return assets;
13896
14558
  }
13897
14559
  function buildLocationComparison(dataA, dataB, sections) {
13898
14560
  const unavailable = [];
@@ -14589,7 +15251,7 @@ function buildOfferPayload(o) {
14589
15251
  var import_zod52 = require("zod");
14590
15252
  var fs7 = __toESM(require("fs"));
14591
15253
  var path6 = __toESM(require("path"));
14592
- function delay3(ms) {
15254
+ function delay4(ms) {
14593
15255
  return new Promise((resolve8) => setTimeout(resolve8, ms));
14594
15256
  }
14595
15257
  var TemplateSchema = import_zod52.z.object({
@@ -14774,7 +15436,7 @@ function registerTemplateDeployerTools(server2, client) {
14774
15436
  errors.push(`Location update: ${errorMessage(e)}`);
14775
15437
  log.push(" \u2717 Location update failed (may need agency-level key)");
14776
15438
  }
14777
- await delay3(200);
15439
+ await delay4(200);
14778
15440
  }
14779
15441
  }
14780
15442
  if (template.tags) {
@@ -14793,7 +15455,7 @@ Tags: ${template.tags.length} to create`);
14793
15455
  log.push(` \u2717 Tag failed: ${tag}`);
14794
15456
  }
14795
15457
  }
14796
- await delay3(100);
15458
+ await delay4(100);
14797
15459
  }
14798
15460
  } else {
14799
15461
  for (const tag of template.tags) {
@@ -14823,7 +15485,7 @@ Custom Fields: ${template.customFields.length} to create`);
14823
15485
  log.push(` \u2717 Field failed: ${field.name}`);
14824
15486
  }
14825
15487
  }
14826
- await delay3(100);
15488
+ await delay4(100);
14827
15489
  }
14828
15490
  } else {
14829
15491
  for (const field of template.customFields) {
@@ -14869,7 +15531,7 @@ Pipelines: ${template.pipelines.length} to create`);
14869
15531
  } catch (e) {
14870
15532
  errors.push(`Pipeline "${resolved2.name}": ${errorMessage(e)}`);
14871
15533
  }
14872
- await delay3(300);
15534
+ await delay4(300);
14873
15535
  }
14874
15536
  }
14875
15537
  }
@@ -14907,7 +15569,7 @@ Workflows: ${workflows.length} to create`);
14907
15569
  errors.push(`Workflow "${resolved2.name}": ${errorMessage(e)}`);
14908
15570
  log.push(` \u2717 Workflow failed: ${resolved2.name}`);
14909
15571
  }
14910
- await delay3(500);
15572
+ await delay4(500);
14911
15573
  }
14912
15574
  }
14913
15575
  }
@@ -14932,7 +15594,7 @@ Calendars: ${template.calendars.length} to create`);
14932
15594
  errors.push(`Calendar "${resolved2.name}": ${errorMessage(e)}`);
14933
15595
  log.push(` \u2717 Calendar failed: ${resolved2.name}`);
14934
15596
  }
14935
- await delay3(200);
15597
+ await delay4(200);
14936
15598
  }
14937
15599
  }
14938
15600
  }
@@ -19411,7 +20073,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
19411
20073
  () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
19412
20074
  );
19413
20075
  registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
19414
- registerAccountExportTools(wrap(ACCOUNT_EXPORT_MODULE), client, registry2);
20076
+ registerAccountExportTools(wrap(ACCOUNT_EXPORT_MODULE), client, registry2, builderClient);
19415
20077
  registerLocationSwitcherTools(
19416
20078
  wrap(LOCATION_SWITCHER_MODULE),
19417
20079
  client,