@elitedcs/ghl-mcp 3.66.2 → 3.68.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/CHANGELOG.md +98 -0
- package/README.md +5 -5
- package/dist/index.js +1441 -443
- package/guide/guide.html +87 -4
- package/package.json +2 -2
- package/templates/action-schemas.json +2 -2
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
|
|
151
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${
|
|
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,
|
|
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
|
|
162
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path15}, retry ${attempt + 1}/${MAX_RETRIES} in ${
|
|
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,
|
|
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 ? "
|
|
1651
|
+
const finishedCount = isFree ? "112" : "242";
|
|
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
|
|
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 187 to 242 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.
|
|
2857
|
+
version: "3.68.0",
|
|
2858
2858
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
2859
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
2859
|
+
description: "GoHighLevel MCP Server for Claude. 242 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
|
|
6191
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${
|
|
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,
|
|
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
|
|
6202
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${urlPath}, retry ${attempt + 1}/${MAX_RETRIES2} in ${
|
|
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,
|
|
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) {
|
|
@@ -6506,8 +6506,10 @@ var FREE_TIER_EXTRA_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
6506
6506
|
// live funnel QA (reads + public URL probes)
|
|
6507
6507
|
"check_blog_slug",
|
|
6508
6508
|
// slug availability lookup
|
|
6509
|
-
"workflow_builder_status"
|
|
6509
|
+
"workflow_builder_status",
|
|
6510
6510
|
// no-Firebase fallback diagnostic (pure status read)
|
|
6511
|
+
"run_checkup"
|
|
6512
|
+
// whole-install checkup: install layer + live capability probe (all reads)
|
|
6511
6513
|
]);
|
|
6512
6514
|
var FREE_TIER_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
6513
6515
|
"export_account"
|
|
@@ -10182,6 +10184,18 @@ function summarizeWebhookWorkflow(workflow) {
|
|
|
10182
10184
|
};
|
|
10183
10185
|
}
|
|
10184
10186
|
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();
|
|
10187
|
+
async function loadWebhooks(client, limit) {
|
|
10188
|
+
const listed = WorkflowListSchema.parse(await client.listWorkflows(limit, 0));
|
|
10189
|
+
const rows = listed.rows ?? [];
|
|
10190
|
+
const webhooks = [];
|
|
10191
|
+
for (const row of rows) {
|
|
10192
|
+
const id = row._id ?? row.id;
|
|
10193
|
+
if (!id) continue;
|
|
10194
|
+
const summary = summarizeWebhookWorkflow(await client.getWorkflow(id));
|
|
10195
|
+
if (summary) webhooks.push(summary);
|
|
10196
|
+
}
|
|
10197
|
+
return { webhooks, scanned: rows.length };
|
|
10198
|
+
}
|
|
10185
10199
|
function registerWebhookTools(server2, builderClient) {
|
|
10186
10200
|
const requireClient = () => {
|
|
10187
10201
|
if (!builderClient) {
|
|
@@ -10191,18 +10205,6 @@ function registerWebhookTools(server2, builderClient) {
|
|
|
10191
10205
|
}
|
|
10192
10206
|
return builderClient;
|
|
10193
10207
|
};
|
|
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
10208
|
server2.tool(
|
|
10207
10209
|
"list_webhooks",
|
|
10208
10210
|
"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,
|
|
@@ -10350,10 +10352,10 @@ function registerWebhookTools(server2, builderClient) {
|
|
|
10350
10352
|
confirm: import_zod34.z.literal("DELETE").describe("Must be 'DELETE' to confirm this destructive action."),
|
|
10351
10353
|
force: import_zod34.z.boolean().optional().describe("Delete even when the workflow contains other actions that would go with it.")
|
|
10352
10354
|
},
|
|
10353
|
-
async ({ webhookId, confirm, force }) => {
|
|
10355
|
+
async ({ webhookId, confirm: confirm2, force }) => {
|
|
10354
10356
|
try {
|
|
10355
10357
|
const client = requireClient();
|
|
10356
|
-
if (
|
|
10358
|
+
if (confirm2 !== "DELETE") throw new Error("Pass confirm: 'DELETE' to delete a webhook.");
|
|
10357
10359
|
const summary = summarizeWebhookWorkflow(await client.getWorkflow(webhookId));
|
|
10358
10360
|
if (!summary) {
|
|
10359
10361
|
throw new Error(
|
|
@@ -12072,7 +12074,7 @@ ${await response.text()}`);
|
|
|
12072
12074
|
);
|
|
12073
12075
|
};
|
|
12074
12076
|
if (priorDataUrl) refuseCreate(livePageVersion);
|
|
12075
|
-
for (let
|
|
12077
|
+
for (let confirm2 = 0; confirm2 < 3; confirm2++) {
|
|
12076
12078
|
await new Promise((r) => setTimeout(r, 1500));
|
|
12077
12079
|
let again = null;
|
|
12078
12080
|
try {
|
|
@@ -13666,8 +13668,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
13666
13668
|
contactIds: import_zod45.z.array(import_zod45.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
|
|
13667
13669
|
confirm: import_zod45.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
|
|
13668
13670
|
},
|
|
13669
|
-
async ({ contactIds, confirm }) => {
|
|
13670
|
-
if (
|
|
13671
|
+
async ({ contactIds, confirm: confirm2 }) => {
|
|
13672
|
+
if (confirm2 !== "DELETE") {
|
|
13671
13673
|
return errorResponse("Safety check failed. Pass confirm: 'DELETE' to proceed.");
|
|
13672
13674
|
}
|
|
13673
13675
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -13688,9 +13690,391 @@ function registerBulkOperationTools(server2, client) {
|
|
|
13688
13690
|
|
|
13689
13691
|
// src/tools/account-export.ts
|
|
13690
13692
|
var import_zod46 = require("zod");
|
|
13693
|
+
|
|
13694
|
+
// src/contact-export.ts
|
|
13695
|
+
var CONTACT_PAGE_SIZE = 100;
|
|
13696
|
+
var DEFAULT_MAX_CONTACTS = 5e3;
|
|
13697
|
+
var MAX_PAGES = 1e3;
|
|
13698
|
+
function isComplete(reason) {
|
|
13699
|
+
return reason === "complete";
|
|
13700
|
+
}
|
|
13701
|
+
function stopReasonSentence(reason, fetched, cap) {
|
|
13702
|
+
switch (reason) {
|
|
13703
|
+
case "complete":
|
|
13704
|
+
return `All ${fetched} contacts were exported.`;
|
|
13705
|
+
case "cap-reached":
|
|
13706
|
+
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.`;
|
|
13707
|
+
case "cursor-exhausted":
|
|
13708
|
+
return `INCOMPLETE: ${fetched} contacts were exported, then GoHighLevel stopped providing a way to ask for the next page. Treat this file as partial.`;
|
|
13709
|
+
case "cursor-stalled":
|
|
13710
|
+
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.`;
|
|
13711
|
+
case "page-ceiling":
|
|
13712
|
+
return `INCOMPLETE: ${fetched} contacts were exported and the internal page ceiling was reached. Treat this file as partial.`;
|
|
13713
|
+
}
|
|
13714
|
+
}
|
|
13691
13715
|
function delay2(ms) {
|
|
13692
13716
|
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
13693
13717
|
}
|
|
13718
|
+
async function fetchAllContacts(client, locationId2, options = {}) {
|
|
13719
|
+
const cap = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
|
|
13720
|
+
const pauseMs = options.pauseMs ?? 100;
|
|
13721
|
+
const rows = [];
|
|
13722
|
+
let startAfter;
|
|
13723
|
+
let startAfterId;
|
|
13724
|
+
let pages = 0;
|
|
13725
|
+
let indexTotal = null;
|
|
13726
|
+
let stopReason = "complete";
|
|
13727
|
+
while (true) {
|
|
13728
|
+
if (pages >= MAX_PAGES) {
|
|
13729
|
+
stopReason = "page-ceiling";
|
|
13730
|
+
break;
|
|
13731
|
+
}
|
|
13732
|
+
const raw = await client.get("/contacts/", {
|
|
13733
|
+
params: {
|
|
13734
|
+
locationId: locationId2,
|
|
13735
|
+
limit: CONTACT_PAGE_SIZE,
|
|
13736
|
+
// Both cursor halves or neither. Sending one alone re-serves page 1.
|
|
13737
|
+
startAfter,
|
|
13738
|
+
startAfterId
|
|
13739
|
+
}
|
|
13740
|
+
});
|
|
13741
|
+
const parsed = ContactSearchResponseSchema.parse(raw);
|
|
13742
|
+
pages += 1;
|
|
13743
|
+
if (pages === 1 && typeof parsed.meta?.total === "number") {
|
|
13744
|
+
indexTotal = parsed.meta.total;
|
|
13745
|
+
}
|
|
13746
|
+
if (parsed.contacts.length === 0) {
|
|
13747
|
+
stopReason = "complete";
|
|
13748
|
+
break;
|
|
13749
|
+
}
|
|
13750
|
+
for (const contact of parsed.contacts) {
|
|
13751
|
+
rows.push(contact);
|
|
13752
|
+
if (rows.length >= cap) break;
|
|
13753
|
+
}
|
|
13754
|
+
if (rows.length >= cap) {
|
|
13755
|
+
stopReason = "cap-reached";
|
|
13756
|
+
break;
|
|
13757
|
+
}
|
|
13758
|
+
const nextAfter = parsed.meta?.startAfter ?? void 0;
|
|
13759
|
+
const nextAfterId = parsed.meta?.startAfterId ?? void 0;
|
|
13760
|
+
if (nextAfter == null || !nextAfterId) {
|
|
13761
|
+
stopReason = "cursor-exhausted";
|
|
13762
|
+
break;
|
|
13763
|
+
}
|
|
13764
|
+
if (nextAfter === startAfter && nextAfterId === startAfterId) {
|
|
13765
|
+
stopReason = "cursor-stalled";
|
|
13766
|
+
break;
|
|
13767
|
+
}
|
|
13768
|
+
startAfter = nextAfter;
|
|
13769
|
+
startAfterId = nextAfterId;
|
|
13770
|
+
if (pauseMs > 0) await delay2(pauseMs);
|
|
13771
|
+
}
|
|
13772
|
+
return {
|
|
13773
|
+
rows,
|
|
13774
|
+
fetched: rows.length,
|
|
13775
|
+
pages,
|
|
13776
|
+
stopReason,
|
|
13777
|
+
complete: isComplete(stopReason),
|
|
13778
|
+
indexTotal
|
|
13779
|
+
};
|
|
13780
|
+
}
|
|
13781
|
+
var PREFERRED_COLUMNS = [
|
|
13782
|
+
"id",
|
|
13783
|
+
"firstName",
|
|
13784
|
+
"lastName",
|
|
13785
|
+
"name",
|
|
13786
|
+
"email",
|
|
13787
|
+
"phone",
|
|
13788
|
+
"companyName",
|
|
13789
|
+
"tags",
|
|
13790
|
+
"source",
|
|
13791
|
+
"type",
|
|
13792
|
+
"dateAdded",
|
|
13793
|
+
"dateUpdated",
|
|
13794
|
+
"address1",
|
|
13795
|
+
"city",
|
|
13796
|
+
"state",
|
|
13797
|
+
"postalCode",
|
|
13798
|
+
"country",
|
|
13799
|
+
"timezone",
|
|
13800
|
+
"dnd"
|
|
13801
|
+
];
|
|
13802
|
+
function neutralizeFormula(text) {
|
|
13803
|
+
return /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
|
|
13804
|
+
}
|
|
13805
|
+
function cell(value) {
|
|
13806
|
+
if (value === null || value === void 0) return "";
|
|
13807
|
+
const raw = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value);
|
|
13808
|
+
const text = neutralizeFormula(raw);
|
|
13809
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
13810
|
+
}
|
|
13811
|
+
function contactsToCsv(rows) {
|
|
13812
|
+
if (rows.length === 0) return "";
|
|
13813
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13814
|
+
for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
|
|
13815
|
+
const preferred = PREFERRED_COLUMNS.filter((c) => seen.has(c));
|
|
13816
|
+
const rest = [...seen].filter((c) => !preferred.includes(c)).sort();
|
|
13817
|
+
const columns = [...preferred, ...rest];
|
|
13818
|
+
const lines = [columns.join(",")];
|
|
13819
|
+
for (const row of rows) lines.push(columns.map((c) => cell(row[c])).join(","));
|
|
13820
|
+
return lines.join("\n");
|
|
13821
|
+
}
|
|
13822
|
+
|
|
13823
|
+
// src/asset-ownership.ts
|
|
13824
|
+
var OWNERSHIP_PREFIX = "Offboarding Owner: ";
|
|
13825
|
+
var OWNER_KINDS = ["client", "agency", "borrowed", "third-party"];
|
|
13826
|
+
var OWNER_MEANING = {
|
|
13827
|
+
client: "The client owns this. It stays with them and they keep paying for it.",
|
|
13828
|
+
agency: "The agency owns this. It leaves with the agency unless it is sold or transferred.",
|
|
13829
|
+
borrowed: "Borrowed for the engagement. It must be returned, disconnected, or replaced at exit.",
|
|
13830
|
+
"third-party": "Owned by an outside vendor. Transfer depends on that vendor's own process."
|
|
13831
|
+
};
|
|
13832
|
+
function readCustomValues(raw) {
|
|
13833
|
+
const box = raw && typeof raw === "object" ? raw : {};
|
|
13834
|
+
const list = Array.isArray(box.customValues) ? box.customValues : [];
|
|
13835
|
+
return list.flatMap((entry) => {
|
|
13836
|
+
if (!entry || typeof entry !== "object") return [];
|
|
13837
|
+
const row = entry;
|
|
13838
|
+
if (typeof row.id !== "string" || typeof row.name !== "string") return [];
|
|
13839
|
+
return [{ id: row.id, name: row.name, value: typeof row.value === "string" ? row.value : "" }];
|
|
13840
|
+
});
|
|
13841
|
+
}
|
|
13842
|
+
function isOwnerKind(value) {
|
|
13843
|
+
return OWNER_KINDS.includes(value);
|
|
13844
|
+
}
|
|
13845
|
+
function encodeOwnership(owner, note) {
|
|
13846
|
+
return note && note.trim() ? `${owner} | ${note.trim()}` : owner;
|
|
13847
|
+
}
|
|
13848
|
+
function decodeOwnership(value) {
|
|
13849
|
+
const [head, ...rest] = value.split("|");
|
|
13850
|
+
const owner = head.trim();
|
|
13851
|
+
const note = rest.join("|").trim();
|
|
13852
|
+
return { owner: isOwnerKind(owner) ? owner : null, note: note || void 0 };
|
|
13853
|
+
}
|
|
13854
|
+
async function readOwnership(client, locationId2) {
|
|
13855
|
+
const values = readCustomValues(await client.get(`/locations/${locationId2}/customValues`));
|
|
13856
|
+
const records = [];
|
|
13857
|
+
const unreadable = [];
|
|
13858
|
+
for (const value of values) {
|
|
13859
|
+
if (!value.name.startsWith(OWNERSHIP_PREFIX)) continue;
|
|
13860
|
+
const asset = value.name.slice(OWNERSHIP_PREFIX.length).trim();
|
|
13861
|
+
const { owner, note } = decodeOwnership(value.value);
|
|
13862
|
+
if (!owner) {
|
|
13863
|
+
unreadable.push(`${asset}: "${value.value}"`);
|
|
13864
|
+
continue;
|
|
13865
|
+
}
|
|
13866
|
+
records.push({ asset, owner, note, customValueId: value.id });
|
|
13867
|
+
}
|
|
13868
|
+
return { records, unreadable };
|
|
13869
|
+
}
|
|
13870
|
+
async function writeOwnership(client, locationId2, entries) {
|
|
13871
|
+
const existing = readCustomValues(await client.get(`/locations/${locationId2}/customValues`));
|
|
13872
|
+
const results = [];
|
|
13873
|
+
for (const entry of entries) {
|
|
13874
|
+
const asset = entry.asset.trim();
|
|
13875
|
+
const name = `${OWNERSHIP_PREFIX}${asset}`;
|
|
13876
|
+
const matches = existing.filter((value) => value.name === name);
|
|
13877
|
+
if (matches.length > 1) {
|
|
13878
|
+
results.push({
|
|
13879
|
+
asset,
|
|
13880
|
+
action: "halted",
|
|
13881
|
+
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.`
|
|
13882
|
+
});
|
|
13883
|
+
continue;
|
|
13884
|
+
}
|
|
13885
|
+
const body = { name, value: encodeOwnership(entry.owner, entry.note) };
|
|
13886
|
+
if (matches.length === 1) {
|
|
13887
|
+
if (!matches[0].id) {
|
|
13888
|
+
results.push({
|
|
13889
|
+
asset,
|
|
13890
|
+
action: "halted",
|
|
13891
|
+
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.`
|
|
13892
|
+
});
|
|
13893
|
+
continue;
|
|
13894
|
+
}
|
|
13895
|
+
await client.put(`/locations/${locationId2}/customValues/${matches[0].id}`, { body });
|
|
13896
|
+
results.push({ asset, action: "updated", customValueId: matches[0].id });
|
|
13897
|
+
continue;
|
|
13898
|
+
}
|
|
13899
|
+
const created = await client.post(`/locations/${locationId2}/customValues`, { body });
|
|
13900
|
+
const createdId = readCreatedId(created);
|
|
13901
|
+
existing.push({ id: createdId ?? "", name, value: body.value });
|
|
13902
|
+
results.push({ asset, action: "created", customValueId: createdId });
|
|
13903
|
+
}
|
|
13904
|
+
return results;
|
|
13905
|
+
}
|
|
13906
|
+
function readCreatedId(raw) {
|
|
13907
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
13908
|
+
const box = raw;
|
|
13909
|
+
if (typeof box.id === "string") return box.id;
|
|
13910
|
+
const nested = box.customValue;
|
|
13911
|
+
if (nested && typeof nested === "object") {
|
|
13912
|
+
const inner = nested.id;
|
|
13913
|
+
if (typeof inner === "string") return inner;
|
|
13914
|
+
}
|
|
13915
|
+
return void 0;
|
|
13916
|
+
}
|
|
13917
|
+
|
|
13918
|
+
// src/handoff-doc.ts
|
|
13919
|
+
function sectionItems(section2) {
|
|
13920
|
+
return Array.isArray(section2) ? section2 : [];
|
|
13921
|
+
}
|
|
13922
|
+
function bullet(lines) {
|
|
13923
|
+
return lines.length ? lines.map((l) => `- ${l}`).join("\n") : "- None found.";
|
|
13924
|
+
}
|
|
13925
|
+
function plainReason(reason) {
|
|
13926
|
+
const withoutBody = reason.split(/[{\n]/)[0];
|
|
13927
|
+
const withoutRoute = withoutBody.replace(/:?\s*(GET|POST|PUT|DELETE)\s+\/\S*/g, "");
|
|
13928
|
+
const cleaned = withoutRoute.replace(/\s+/g, " ").replace(/[:\s]+$/, "").trim();
|
|
13929
|
+
return cleaned || "the account did not return this section";
|
|
13930
|
+
}
|
|
13931
|
+
function inventoryTable(inventory) {
|
|
13932
|
+
const rows = Object.entries(inventory).map(([section2, data]) => {
|
|
13933
|
+
if (data.unavailable) return `| ${section2} | not read | ${plainReason(data.unavailable)} |`;
|
|
13934
|
+
const sample = data.names.slice(0, 8).join(", ");
|
|
13935
|
+
const more = data.names.length > 8 ? `, and ${data.names.length - 8} more` : "";
|
|
13936
|
+
return `| ${section2} | ${data.count ?? data.names.length} | ${sample || "none"}${more} |`;
|
|
13937
|
+
});
|
|
13938
|
+
return ["| What | How many | Which ones |", "| --- | --- | --- |", ...rows].join("\n");
|
|
13939
|
+
}
|
|
13940
|
+
function sectionList(section2, render) {
|
|
13941
|
+
if (!Array.isArray(section2)) {
|
|
13942
|
+
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.`;
|
|
13943
|
+
}
|
|
13944
|
+
return bullet(section2.map(render));
|
|
13945
|
+
}
|
|
13946
|
+
function buildHandoffDoc(input) {
|
|
13947
|
+
const {
|
|
13948
|
+
accountName,
|
|
13949
|
+
locationId: locationId2,
|
|
13950
|
+
generatedAt,
|
|
13951
|
+
contacts,
|
|
13952
|
+
inventory,
|
|
13953
|
+
users,
|
|
13954
|
+
phoneNumbers,
|
|
13955
|
+
socialAccounts,
|
|
13956
|
+
webhooks,
|
|
13957
|
+
webhookScanPartial,
|
|
13958
|
+
ownership
|
|
13959
|
+
} = input;
|
|
13960
|
+
const ownedBy = (kind) => ownership.recorded.filter((r) => r.owner === kind);
|
|
13961
|
+
const ownershipSection = ownership.recorded.length ? ["client", "agency", "borrowed", "third-party"].map((kind) => {
|
|
13962
|
+
const items = ownedBy(kind);
|
|
13963
|
+
if (!items.length) return "";
|
|
13964
|
+
return `**${kind}** ${OWNER_MEANING[kind]}
|
|
13965
|
+
|
|
13966
|
+
${bullet(
|
|
13967
|
+
items.map((i) => i.note ? `${i.asset} (${i.note})` : i.asset)
|
|
13968
|
+
)}`;
|
|
13969
|
+
}).filter(Boolean).join("\n\n") : "Nothing has been recorded yet. Every asset below is unanswered.";
|
|
13970
|
+
const unknownSection = ownership.unknown.length ? `These exist in the account but nobody has said who owns them. Answer them once and they stay answered:
|
|
13971
|
+
|
|
13972
|
+
${bullet(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
|
|
13973
|
+
const webhookSection = sectionList(webhooks, (w) => `${w.name} sends to ${w.url}${w.events.length ? ` when: ${w.events.join(", ")}` : ""}`) + (webhookScanPartial ? `
|
|
13974
|
+
|
|
13975
|
+
**This list may be incomplete.** ${webhookScanPartial}` : "");
|
|
13976
|
+
return `# Account handoff: ${accountName}
|
|
13977
|
+
|
|
13978
|
+
Prepared ${generatedAt}. GoHighLevel account ID ${locationId2}.
|
|
13979
|
+
|
|
13980
|
+
This document describes what is inside this account, who can get into it, and
|
|
13981
|
+
what has to change hands. It was generated from the live account, not from
|
|
13982
|
+
memory.
|
|
13983
|
+
|
|
13984
|
+
## Read this first
|
|
13985
|
+
|
|
13986
|
+
${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:
|
|
13987
|
+
|
|
13988
|
+
1. **No passwords or API keys.** GoHighLevel does not hand out key values to any
|
|
13989
|
+
tool, including this one. What you get instead is a list of every place a key
|
|
13990
|
+
exists, so you can rotate them. That list is at the end.
|
|
13991
|
+
2. **No opinion about who owns what**, beyond what somebody actually recorded.
|
|
13992
|
+
Anything unanswered is listed as unanswered.
|
|
13993
|
+
3. **No guesses about what is missing.** If a section could not be read, it says
|
|
13994
|
+
so and why, rather than showing zero.
|
|
13995
|
+
|
|
13996
|
+
## Contacts
|
|
13997
|
+
|
|
13998
|
+
${contacts.sentence}${contacts.indexTotal !== null ? `
|
|
13999
|
+
|
|
14000
|
+
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.` : ""}
|
|
14001
|
+
|
|
14002
|
+
## What is in the account
|
|
14003
|
+
|
|
14004
|
+
${inventoryTable(inventory)}
|
|
14005
|
+
|
|
14006
|
+
## Who can get in
|
|
14007
|
+
|
|
14008
|
+
### People with logins
|
|
14009
|
+
|
|
14010
|
+
${sectionList(users, (u) => `${u.name} (${u.email})${u.roles ? `, ${u.roles}` : ""}`)}
|
|
14011
|
+
|
|
14012
|
+
Every person above can log into this account right now. Remove the ones who
|
|
14013
|
+
should not be able to, on the day the relationship ends, not later.
|
|
14014
|
+
|
|
14015
|
+
### Phone numbers
|
|
14016
|
+
|
|
14017
|
+
${sectionList(phoneNumbers, (p) => p.label ? `${p.number} (${p.label})` : p.number)}
|
|
14018
|
+
|
|
14019
|
+
Numbers are rented from GoHighLevel and stop working if the account lapses. If
|
|
14020
|
+
the client needs to keep a number, it has to be ported out before then, and
|
|
14021
|
+
porting takes days to weeks.
|
|
14022
|
+
|
|
14023
|
+
### Connected accounts
|
|
14024
|
+
|
|
14025
|
+
${sectionList(socialAccounts, (s) => `${s.platform}: ${s.name}`)}
|
|
14026
|
+
|
|
14027
|
+
These connections were authorised by a person. If that person leaves, the
|
|
14028
|
+
connection can break even though nothing in GoHighLevel changed.
|
|
14029
|
+
|
|
14030
|
+
### Outbound webhooks
|
|
14031
|
+
|
|
14032
|
+
${webhookSection}
|
|
14033
|
+
|
|
14034
|
+
Each of these sends data out of this account to somewhere else. Confirm the
|
|
14035
|
+
receiving system should still be receiving it.
|
|
14036
|
+
|
|
14037
|
+
## Who owns what
|
|
14038
|
+
|
|
14039
|
+
${ownershipSection}
|
|
14040
|
+
|
|
14041
|
+
### Not yet answered
|
|
14042
|
+
|
|
14043
|
+
${unknownSection}
|
|
14044
|
+
${ownership.unreadable.length ? `
|
|
14045
|
+
### Recorded but unreadable
|
|
14046
|
+
|
|
14047
|
+
Somebody edited these by hand and the answer no longer parses. They need a human:
|
|
14048
|
+
|
|
14049
|
+
${bullet(ownership.unreadable)}
|
|
14050
|
+
` : ""}
|
|
14051
|
+
## Rotate these on the way out
|
|
14052
|
+
|
|
14053
|
+
Key values cannot be read by any tool, so this is a checklist of places to go
|
|
14054
|
+
and change something, not a list of secrets.
|
|
14055
|
+
|
|
14056
|
+
- The GoHighLevel Private Integration key for this account. Anyone holding the
|
|
14057
|
+
old one keeps full access until it is deleted.
|
|
14058
|
+
- Any Firebase or agency-level credential used to reach this account.
|
|
14059
|
+
- Every connected account listed above, if the departing party authorised it.
|
|
14060
|
+
- Every webhook destination above, if it authenticates with a shared secret.
|
|
14061
|
+
- Any payment processor, email sending domain, or advertising account attached
|
|
14062
|
+
to this business but living outside GoHighLevel.
|
|
14063
|
+
|
|
14064
|
+
## What to do next
|
|
14065
|
+
|
|
14066
|
+
1. Save the contact export alongside this document.
|
|
14067
|
+
2. Answer anything under "Not yet answered" and re-run the kit so the answers
|
|
14068
|
+
are stored in the account itself.
|
|
14069
|
+
3. Work down the rotation list above.
|
|
14070
|
+
4. Remove the logins that should no longer exist.
|
|
14071
|
+
`;
|
|
14072
|
+
}
|
|
14073
|
+
|
|
14074
|
+
// src/tools/account-export.ts
|
|
14075
|
+
function delay3(ms) {
|
|
14076
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
14077
|
+
}
|
|
13694
14078
|
function builderSectionAvailability(builderLocationId, requestedLocationId) {
|
|
13695
14079
|
if (!builderLocationId) {
|
|
13696
14080
|
return {
|
|
@@ -13726,7 +14110,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13726
14110
|
} catch (e) {
|
|
13727
14111
|
report.location = `Error fetching: ${errorMessage(e)}`;
|
|
13728
14112
|
}
|
|
13729
|
-
await
|
|
14113
|
+
await delay3(100);
|
|
13730
14114
|
try {
|
|
13731
14115
|
const contacts = await client.get("/contacts/", {
|
|
13732
14116
|
params: { locationId: locId, limit: includeContacts ? 100 : 1 }
|
|
@@ -13741,7 +14125,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13741
14125
|
} catch (e) {
|
|
13742
14126
|
report.contacts = `Error fetching: ${errorMessage(e)}`;
|
|
13743
14127
|
}
|
|
13744
|
-
await
|
|
14128
|
+
await delay3(100);
|
|
13745
14129
|
try {
|
|
13746
14130
|
report.pipelines = await client.get("/opportunities/pipelines", {
|
|
13747
14131
|
params: { locationId: locId }
|
|
@@ -13749,7 +14133,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13749
14133
|
} catch (e) {
|
|
13750
14134
|
report.pipelines = `Error fetching: ${errorMessage(e)}`;
|
|
13751
14135
|
}
|
|
13752
|
-
await
|
|
14136
|
+
await delay3(100);
|
|
13753
14137
|
try {
|
|
13754
14138
|
report.workflows = await client.get("/workflows/", {
|
|
13755
14139
|
params: { locationId: locId }
|
|
@@ -13757,7 +14141,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13757
14141
|
} catch (e) {
|
|
13758
14142
|
report.workflows = `Error fetching: ${errorMessage(e)}`;
|
|
13759
14143
|
}
|
|
13760
|
-
await
|
|
14144
|
+
await delay3(100);
|
|
13761
14145
|
const builderUse = builderSectionAvailability(builderClient?.locationId, locId);
|
|
13762
14146
|
if (builderUse.usable && builderClient) {
|
|
13763
14147
|
try {
|
|
@@ -13765,7 +14149,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13765
14149
|
} catch (e) {
|
|
13766
14150
|
report.workflowsFull = `Error fetching: ${errorMessage(e)}`;
|
|
13767
14151
|
}
|
|
13768
|
-
await
|
|
14152
|
+
await delay3(100);
|
|
13769
14153
|
} else {
|
|
13770
14154
|
report.workflowsFull = builderUse.reason;
|
|
13771
14155
|
}
|
|
@@ -13781,7 +14165,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13781
14165
|
} catch (e) {
|
|
13782
14166
|
report.funnels = `Error fetching: ${errorMessage(e)}`;
|
|
13783
14167
|
}
|
|
13784
|
-
await
|
|
14168
|
+
await delay3(100);
|
|
13785
14169
|
} else {
|
|
13786
14170
|
try {
|
|
13787
14171
|
report.funnels = await client.get("/funnels/", {
|
|
@@ -13798,25 +14182,25 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13798
14182
|
} catch (e) {
|
|
13799
14183
|
report.forms = `Error fetching: ${errorMessage(e)}`;
|
|
13800
14184
|
}
|
|
13801
|
-
await
|
|
14185
|
+
await delay3(100);
|
|
13802
14186
|
try {
|
|
13803
14187
|
report.customFields = await client.get("/locations/" + locId + "/customFields");
|
|
13804
14188
|
} catch (e) {
|
|
13805
14189
|
report.customFields = `Error fetching: ${errorMessage(e)}`;
|
|
13806
14190
|
}
|
|
13807
|
-
await
|
|
14191
|
+
await delay3(100);
|
|
13808
14192
|
try {
|
|
13809
14193
|
report.customValues = await client.get("/locations/" + locId + "/customValues");
|
|
13810
14194
|
} catch (e) {
|
|
13811
14195
|
report.customValues = `Error fetching: ${errorMessage(e)}`;
|
|
13812
14196
|
}
|
|
13813
|
-
await
|
|
14197
|
+
await delay3(100);
|
|
13814
14198
|
try {
|
|
13815
14199
|
report.tags = await client.get("/locations/" + locId + "/tags");
|
|
13816
14200
|
} catch (e) {
|
|
13817
14201
|
report.tags = `Error fetching: ${errorMessage(e)}`;
|
|
13818
14202
|
}
|
|
13819
|
-
await
|
|
14203
|
+
await delay3(100);
|
|
13820
14204
|
try {
|
|
13821
14205
|
report.calendars = await client.get("/calendars/", {
|
|
13822
14206
|
params: { locationId: locId }
|
|
@@ -13824,7 +14208,7 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13824
14208
|
} catch (e) {
|
|
13825
14209
|
report.calendars = `Error fetching: ${errorMessage(e)}`;
|
|
13826
14210
|
}
|
|
13827
|
-
await
|
|
14211
|
+
await delay3(100);
|
|
13828
14212
|
try {
|
|
13829
14213
|
report.users = await client.get("/users/", {
|
|
13830
14214
|
params: { locationId: locId }
|
|
@@ -13870,13 +14254,13 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13870
14254
|
const fetchData = async (locId) => {
|
|
13871
14255
|
const data = {};
|
|
13872
14256
|
data.pipelines = await read(() => client.get("/opportunities/pipelines", { params: { locationId: locId } }));
|
|
13873
|
-
await
|
|
14257
|
+
await delay3(100);
|
|
13874
14258
|
data.workflows = await read(() => client.get("/workflows/", { params: { locationId: locId } }));
|
|
13875
|
-
await
|
|
14259
|
+
await delay3(100);
|
|
13876
14260
|
data.customFields = await read(() => client.get(`/locations/${locId}/customFields`));
|
|
13877
|
-
await
|
|
14261
|
+
await delay3(100);
|
|
13878
14262
|
data.tags = await read(() => client.get(`/locations/${locId}/tags`));
|
|
13879
|
-
await
|
|
14263
|
+
await delay3(100);
|
|
13880
14264
|
data.forms = await read(() => client.get("/forms/", { params: { locationId: locId } }));
|
|
13881
14265
|
return data;
|
|
13882
14266
|
};
|
|
@@ -13909,6 +14293,270 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
13909
14293
|
}
|
|
13910
14294
|
}
|
|
13911
14295
|
);
|
|
14296
|
+
server2.tool(
|
|
14297
|
+
"build_offboarding_kit",
|
|
14298
|
+
"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.",
|
|
14299
|
+
{
|
|
14300
|
+
locationId: import_zod46.z.string().describe("Sub-account to build the kit for. REQUIRED \u2014 this tool never falls back to the active account."),
|
|
14301
|
+
contacts: import_zod46.z.enum(["summary", "csv"]).optional().describe(
|
|
14302
|
+
"'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."
|
|
14303
|
+
),
|
|
14304
|
+
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.`)
|
|
14305
|
+
},
|
|
14306
|
+
async ({ locationId: locationId2, contacts, maxContacts }) => {
|
|
14307
|
+
const originalKey = client.getApiKey();
|
|
14308
|
+
const registered = registry2?.getToken(locationId2);
|
|
14309
|
+
const usedRegisteredKey = Boolean(registered?.apiKey && registered.apiKey !== originalKey);
|
|
14310
|
+
if (usedRegisteredKey && registered?.apiKey) client.setApiKey(registered.apiKey);
|
|
14311
|
+
try {
|
|
14312
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14313
|
+
let accountName = locationId2;
|
|
14314
|
+
let location = null;
|
|
14315
|
+
try {
|
|
14316
|
+
location = await client.get(`/locations/${locationId2}`);
|
|
14317
|
+
const box = location && typeof location === "object" ? location : {};
|
|
14318
|
+
const inner = box.location && typeof box.location === "object" ? box.location : box;
|
|
14319
|
+
if (typeof inner.name === "string" && inner.name) accountName = inner.name;
|
|
14320
|
+
} catch (e) {
|
|
14321
|
+
location = `Error fetching: ${errorMessage(e)}`;
|
|
14322
|
+
}
|
|
14323
|
+
await delay3(100);
|
|
14324
|
+
const inventory = {};
|
|
14325
|
+
const section2 = async (label, fn) => {
|
|
14326
|
+
try {
|
|
14327
|
+
const names = extractNames(await fn());
|
|
14328
|
+
inventory[label] = { count: names.length, names };
|
|
14329
|
+
} catch (e) {
|
|
14330
|
+
inventory[label] = { count: null, names: [], unavailable: errorMessage(e) };
|
|
14331
|
+
}
|
|
14332
|
+
await delay3(100);
|
|
14333
|
+
};
|
|
14334
|
+
await section2("pipelines", () => client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }));
|
|
14335
|
+
await section2("workflows", () => client.get("/workflows/", { params: { locationId: locationId2 } }));
|
|
14336
|
+
await section2("funnels", () => client.get("/funnels/", { params: { locationId: locationId2 } }));
|
|
14337
|
+
await section2("forms", () => client.get("/forms/", { params: { locationId: locationId2 } }));
|
|
14338
|
+
await section2("calendars", () => client.get("/calendars/", { params: { locationId: locationId2 } }));
|
|
14339
|
+
await section2("customFields", () => client.get(`/locations/${locationId2}/customFields`));
|
|
14340
|
+
await section2("customValues", () => client.get(`/locations/${locationId2}/customValues`));
|
|
14341
|
+
await section2("tags", () => client.get(`/locations/${locationId2}/tags`));
|
|
14342
|
+
const users = await readUsers(client, locationId2);
|
|
14343
|
+
await delay3(100);
|
|
14344
|
+
const phoneNumbers = await readPhoneNumbers(client, locationId2);
|
|
14345
|
+
await delay3(100);
|
|
14346
|
+
const socialAccounts = await readSocialAccounts(client, locationId2);
|
|
14347
|
+
await delay3(100);
|
|
14348
|
+
const WEBHOOK_SCAN_LIMIT = 50;
|
|
14349
|
+
const builderUse = builderSectionAvailability(builderClient?.locationId, locationId2);
|
|
14350
|
+
let webhooks;
|
|
14351
|
+
let webhookScanPartial;
|
|
14352
|
+
if (builderUse.usable && builderClient) {
|
|
14353
|
+
try {
|
|
14354
|
+
const scan = await loadWebhooks(builderClient, WEBHOOK_SCAN_LIMIT);
|
|
14355
|
+
const stillOurs = builderSectionAvailability(builderClient.locationId, locationId2);
|
|
14356
|
+
if (!stillOurs.usable) {
|
|
14357
|
+
webhooks = { unavailable: `The active account changed while webhooks were being read, so the result was discarded. ${stillOurs.reason ?? ""}` };
|
|
14358
|
+
} else {
|
|
14359
|
+
webhooks = scan.webhooks.map((w) => ({
|
|
14360
|
+
name: w.name,
|
|
14361
|
+
url: w.actions[0]?.url ?? "",
|
|
14362
|
+
events: w.events
|
|
14363
|
+
}));
|
|
14364
|
+
if (scan.scanned >= WEBHOOK_SCAN_LIMIT) {
|
|
14365
|
+
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.`;
|
|
14366
|
+
}
|
|
14367
|
+
}
|
|
14368
|
+
} catch (e) {
|
|
14369
|
+
webhooks = { unavailable: `Error reading webhooks: ${errorMessage(e)}` };
|
|
14370
|
+
}
|
|
14371
|
+
} else {
|
|
14372
|
+
webhooks = { unavailable: builderUse.reason ?? "Not read." };
|
|
14373
|
+
}
|
|
14374
|
+
let ownershipRecords = { records: [], unreadable: [] };
|
|
14375
|
+
let ownershipError;
|
|
14376
|
+
try {
|
|
14377
|
+
ownershipRecords = await readOwnership(client, locationId2);
|
|
14378
|
+
} catch (e) {
|
|
14379
|
+
ownershipError = errorMessage(e);
|
|
14380
|
+
}
|
|
14381
|
+
const discovered = discoverAssets({ phoneNumbers, socialAccounts, inventory });
|
|
14382
|
+
const answered = new Set(ownershipRecords.records.map((r) => r.asset.toLowerCase()));
|
|
14383
|
+
const unanswered = discovered.filter((asset) => !answered.has(asset.toLowerCase()));
|
|
14384
|
+
const cap = maxContacts ?? DEFAULT_MAX_CONTACTS;
|
|
14385
|
+
let contactResult;
|
|
14386
|
+
let contactSentence;
|
|
14387
|
+
try {
|
|
14388
|
+
contactResult = await fetchAllContacts(client, locationId2, { maxContacts: cap });
|
|
14389
|
+
contactSentence = stopReasonSentence(contactResult.stopReason, contactResult.fetched, cap);
|
|
14390
|
+
} catch (e) {
|
|
14391
|
+
contactResult = { rows: [], fetched: 0, pages: 0, stopReason: "cursor-exhausted", complete: false, indexTotal: null };
|
|
14392
|
+
contactSentence = `INCOMPLETE: the contact export failed and returned nothing. Reason: ${errorMessage(e)}`;
|
|
14393
|
+
}
|
|
14394
|
+
const handoffDoc = buildHandoffDoc({
|
|
14395
|
+
accountName,
|
|
14396
|
+
locationId: locationId2,
|
|
14397
|
+
generatedAt,
|
|
14398
|
+
contacts: {
|
|
14399
|
+
exported: contactResult.fetched,
|
|
14400
|
+
indexTotal: contactResult.indexTotal,
|
|
14401
|
+
complete: contactResult.complete,
|
|
14402
|
+
sentence: contactSentence
|
|
14403
|
+
},
|
|
14404
|
+
inventory,
|
|
14405
|
+
users,
|
|
14406
|
+
phoneNumbers,
|
|
14407
|
+
socialAccounts,
|
|
14408
|
+
webhooks,
|
|
14409
|
+
webhookScanPartial,
|
|
14410
|
+
ownership: {
|
|
14411
|
+
recorded: ownershipRecords.records,
|
|
14412
|
+
unknown: unanswered,
|
|
14413
|
+
unreadable: ownershipRecords.unreadable
|
|
14414
|
+
}
|
|
14415
|
+
});
|
|
14416
|
+
return jsonResponse({
|
|
14417
|
+
account: { locationId: locationId2, name: accountName, generatedAt, usedRegisteredKey },
|
|
14418
|
+
contacts: {
|
|
14419
|
+
exported: contactResult.fetched,
|
|
14420
|
+
ghlIndexTotal: contactResult.indexTotal,
|
|
14421
|
+
complete: contactResult.complete,
|
|
14422
|
+
stopReason: contactResult.stopReason,
|
|
14423
|
+
pagesRead: contactResult.pages,
|
|
14424
|
+
statement: contactSentence,
|
|
14425
|
+
...contacts === "csv" ? { csv: contactsToCsv(contactResult.rows) } : {
|
|
14426
|
+
csvNote: "Run again with contacts:'csv' to get the full export as CSV text to save."
|
|
14427
|
+
}
|
|
14428
|
+
},
|
|
14429
|
+
inventory,
|
|
14430
|
+
accessMap: {
|
|
14431
|
+
users,
|
|
14432
|
+
phoneNumbers,
|
|
14433
|
+
socialAccounts,
|
|
14434
|
+
webhooks,
|
|
14435
|
+
...webhookScanPartial ? { webhookScanPartial } : {}
|
|
14436
|
+
},
|
|
14437
|
+
ownership: {
|
|
14438
|
+
recorded: ownershipRecords.records,
|
|
14439
|
+
unanswered,
|
|
14440
|
+
unreadable: ownershipRecords.unreadable,
|
|
14441
|
+
...ownershipError ? { error: ownershipError } : {},
|
|
14442
|
+
howToAnswer: "Record these with record_asset_ownership so the next run needs no interview."
|
|
14443
|
+
},
|
|
14444
|
+
keyRotationNote: "Key VALUES are not retrievable from GoHighLevel by any tool. The handoff document lists where each key lives so they can be rotated.",
|
|
14445
|
+
handoffDoc
|
|
14446
|
+
});
|
|
14447
|
+
} catch (error) {
|
|
14448
|
+
return errorResponse(error);
|
|
14449
|
+
} finally {
|
|
14450
|
+
client.setApiKey(originalKey);
|
|
14451
|
+
}
|
|
14452
|
+
}
|
|
14453
|
+
);
|
|
14454
|
+
server2.tool(
|
|
14455
|
+
"record_asset_ownership",
|
|
14456
|
+
"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.",
|
|
14457
|
+
{
|
|
14458
|
+
locationId: import_zod46.z.string().describe("Sub-account to record ownership in. REQUIRED \u2014 never falls back to the active account."),
|
|
14459
|
+
assets: import_zod46.z.array(
|
|
14460
|
+
import_zod46.z.object({
|
|
14461
|
+
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'."),
|
|
14462
|
+
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."),
|
|
14463
|
+
note: import_zod46.z.string().optional().describe("Anything the next person needs, e.g. 'registered on the client's own GoDaddy account'.")
|
|
14464
|
+
})
|
|
14465
|
+
).min(1).describe("One entry per asset. Send several at once.")
|
|
14466
|
+
},
|
|
14467
|
+
async ({ locationId: locationId2, assets }) => {
|
|
14468
|
+
const originalKey = client.getApiKey();
|
|
14469
|
+
const registered = registry2?.getToken(locationId2);
|
|
14470
|
+
if (registered?.apiKey && registered.apiKey !== originalKey) client.setApiKey(registered.apiKey);
|
|
14471
|
+
try {
|
|
14472
|
+
const results = await writeOwnership(client, locationId2, assets);
|
|
14473
|
+
const halted = results.filter((r) => r.action === "halted");
|
|
14474
|
+
return jsonResponse({
|
|
14475
|
+
locationId: locationId2,
|
|
14476
|
+
recorded: results.filter((r) => r.action !== "halted"),
|
|
14477
|
+
...halted.length ? { halted, warning: "Some assets were NOT recorded. See halted for which, and why." } : {}
|
|
14478
|
+
});
|
|
14479
|
+
} catch (error) {
|
|
14480
|
+
return errorResponse(error);
|
|
14481
|
+
} finally {
|
|
14482
|
+
client.setApiKey(originalKey);
|
|
14483
|
+
}
|
|
14484
|
+
}
|
|
14485
|
+
);
|
|
14486
|
+
}
|
|
14487
|
+
function extractNames(payload) {
|
|
14488
|
+
const list = Array.isArray(payload) ? payload : payload && typeof payload === "object" ? Object.values(payload).find(Array.isArray) : void 0;
|
|
14489
|
+
if (!Array.isArray(list)) return [];
|
|
14490
|
+
return list.map(
|
|
14491
|
+
(item) => item && typeof item === "object" ? String(item.name ?? item.id ?? "") : String(item)
|
|
14492
|
+
).filter(Boolean);
|
|
14493
|
+
}
|
|
14494
|
+
async function readSection(read) {
|
|
14495
|
+
try {
|
|
14496
|
+
return await read();
|
|
14497
|
+
} catch (e) {
|
|
14498
|
+
return { unavailable: errorMessage(e) };
|
|
14499
|
+
}
|
|
14500
|
+
}
|
|
14501
|
+
async function readUsers(client, locationId2) {
|
|
14502
|
+
return readSection(async () => {
|
|
14503
|
+
const raw = await client.get("/users/", { params: { locationId: locationId2 } });
|
|
14504
|
+
const box = raw && typeof raw === "object" ? raw : {};
|
|
14505
|
+
const list = Array.isArray(box.users) ? box.users : [];
|
|
14506
|
+
return list.flatMap((entry) => {
|
|
14507
|
+
if (!entry || typeof entry !== "object") return [];
|
|
14508
|
+
const u = entry;
|
|
14509
|
+
const roles = u.roles && typeof u.roles === "object" ? String(u.roles.role ?? "") : "";
|
|
14510
|
+
return [{
|
|
14511
|
+
name: String(u.name ?? `${u.firstName ?? ""} ${u.lastName ?? ""}`).trim() || "(unnamed)",
|
|
14512
|
+
email: String(u.email ?? ""),
|
|
14513
|
+
roles
|
|
14514
|
+
}];
|
|
14515
|
+
});
|
|
14516
|
+
});
|
|
14517
|
+
}
|
|
14518
|
+
async function readPhoneNumbers(client, locationId2) {
|
|
14519
|
+
return readSection(async () => {
|
|
14520
|
+
const raw = await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } });
|
|
14521
|
+
const box = raw && typeof raw === "object" ? raw : {};
|
|
14522
|
+
const list = Array.isArray(box.phoneNumbers) ? box.phoneNumbers : [];
|
|
14523
|
+
return list.flatMap((entry) => {
|
|
14524
|
+
if (!entry || typeof entry !== "object") return [];
|
|
14525
|
+
const n = entry;
|
|
14526
|
+
const number = String(n.value ?? n.number ?? "");
|
|
14527
|
+
return number ? [{ number, label: n.title ? String(n.title) : void 0 }] : [];
|
|
14528
|
+
});
|
|
14529
|
+
});
|
|
14530
|
+
}
|
|
14531
|
+
async function readSocialAccounts(client, locationId2) {
|
|
14532
|
+
return readSection(async () => {
|
|
14533
|
+
const raw = await client.get(`/social-media-posting/${locationId2}/accounts`);
|
|
14534
|
+
const box = raw && typeof raw === "object" ? raw : {};
|
|
14535
|
+
const results = box.results && typeof box.results === "object" ? box.results : {};
|
|
14536
|
+
const list = Array.isArray(results.accounts) ? results.accounts : [];
|
|
14537
|
+
return list.flatMap((entry) => {
|
|
14538
|
+
if (!entry || typeof entry !== "object") return [];
|
|
14539
|
+
const a = entry;
|
|
14540
|
+
return [{ platform: String(a.platform ?? "unknown"), name: String(a.name ?? a.id ?? "") }];
|
|
14541
|
+
});
|
|
14542
|
+
});
|
|
14543
|
+
}
|
|
14544
|
+
var OFF_PLATFORM_ASSETS = [
|
|
14545
|
+
"Website domain",
|
|
14546
|
+
"Logo and brand files",
|
|
14547
|
+
"Meta pixel or ad account",
|
|
14548
|
+
"Google Analytics",
|
|
14549
|
+
"Payment processor account",
|
|
14550
|
+
"Email sending domain"
|
|
14551
|
+
];
|
|
14552
|
+
function discoverAssets(input) {
|
|
14553
|
+
const assets = [];
|
|
14554
|
+
for (const p of sectionItems(input.phoneNumbers)) assets.push(`Phone number ${p.number}`);
|
|
14555
|
+
for (const s of sectionItems(input.socialAccounts)) assets.push(`${s.platform} account ${s.name}`.trim());
|
|
14556
|
+
const funnels = input.inventory.funnels;
|
|
14557
|
+
if (funnels && !funnels.unavailable) for (const name of funnels.names) assets.push(`Funnel "${name}"`);
|
|
14558
|
+
assets.push(...OFF_PLATFORM_ASSETS);
|
|
14559
|
+
return assets;
|
|
13912
14560
|
}
|
|
13913
14561
|
function buildLocationComparison(dataA, dataB, sections) {
|
|
13914
14562
|
const unavailable = [];
|
|
@@ -14605,7 +15253,7 @@ function buildOfferPayload(o) {
|
|
|
14605
15253
|
var import_zod52 = require("zod");
|
|
14606
15254
|
var fs7 = __toESM(require("fs"));
|
|
14607
15255
|
var path6 = __toESM(require("path"));
|
|
14608
|
-
function
|
|
15256
|
+
function delay4(ms) {
|
|
14609
15257
|
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
14610
15258
|
}
|
|
14611
15259
|
var TemplateSchema = import_zod52.z.object({
|
|
@@ -14790,7 +15438,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
14790
15438
|
errors.push(`Location update: ${errorMessage(e)}`);
|
|
14791
15439
|
log.push(" \u2717 Location update failed (may need agency-level key)");
|
|
14792
15440
|
}
|
|
14793
|
-
await
|
|
15441
|
+
await delay4(200);
|
|
14794
15442
|
}
|
|
14795
15443
|
}
|
|
14796
15444
|
if (template.tags) {
|
|
@@ -14809,7 +15457,7 @@ Tags: ${template.tags.length} to create`);
|
|
|
14809
15457
|
log.push(` \u2717 Tag failed: ${tag}`);
|
|
14810
15458
|
}
|
|
14811
15459
|
}
|
|
14812
|
-
await
|
|
15460
|
+
await delay4(100);
|
|
14813
15461
|
}
|
|
14814
15462
|
} else {
|
|
14815
15463
|
for (const tag of template.tags) {
|
|
@@ -14839,7 +15487,7 @@ Custom Fields: ${template.customFields.length} to create`);
|
|
|
14839
15487
|
log.push(` \u2717 Field failed: ${field.name}`);
|
|
14840
15488
|
}
|
|
14841
15489
|
}
|
|
14842
|
-
await
|
|
15490
|
+
await delay4(100);
|
|
14843
15491
|
}
|
|
14844
15492
|
} else {
|
|
14845
15493
|
for (const field of template.customFields) {
|
|
@@ -14885,7 +15533,7 @@ Pipelines: ${template.pipelines.length} to create`);
|
|
|
14885
15533
|
} catch (e) {
|
|
14886
15534
|
errors.push(`Pipeline "${resolved2.name}": ${errorMessage(e)}`);
|
|
14887
15535
|
}
|
|
14888
|
-
await
|
|
15536
|
+
await delay4(300);
|
|
14889
15537
|
}
|
|
14890
15538
|
}
|
|
14891
15539
|
}
|
|
@@ -14923,7 +15571,7 @@ Workflows: ${workflows.length} to create`);
|
|
|
14923
15571
|
errors.push(`Workflow "${resolved2.name}": ${errorMessage(e)}`);
|
|
14924
15572
|
log.push(` \u2717 Workflow failed: ${resolved2.name}`);
|
|
14925
15573
|
}
|
|
14926
|
-
await
|
|
15574
|
+
await delay4(500);
|
|
14927
15575
|
}
|
|
14928
15576
|
}
|
|
14929
15577
|
}
|
|
@@ -14948,7 +15596,7 @@ Calendars: ${template.calendars.length} to create`);
|
|
|
14948
15596
|
errors.push(`Calendar "${resolved2.name}": ${errorMessage(e)}`);
|
|
14949
15597
|
log.push(` \u2717 Calendar failed: ${resolved2.name}`);
|
|
14950
15598
|
}
|
|
14951
|
-
await
|
|
15599
|
+
await delay4(200);
|
|
14952
15600
|
}
|
|
14953
15601
|
}
|
|
14954
15602
|
}
|
|
@@ -15603,167 +16251,507 @@ async function getVersionStatus(installed) {
|
|
|
15603
16251
|
}
|
|
15604
16252
|
|
|
15605
16253
|
// src/tools/diagnostics.ts
|
|
16254
|
+
async function collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport) {
|
|
16255
|
+
const checks = [];
|
|
16256
|
+
const versionPromise = getVersionStatus(installedVersion);
|
|
16257
|
+
const apiKeyPromise = (async () => {
|
|
16258
|
+
const locId = client.defaultLocationId;
|
|
16259
|
+
if (!locId) {
|
|
16260
|
+
return { name: "GHL API key", status: "warn", detail: `Can't validate without a default location ID. Set GHL_LOCATION_ID or run setup_ghl_mcp.` };
|
|
16261
|
+
}
|
|
16262
|
+
try {
|
|
16263
|
+
const response = await fetch(`https://services.leadconnectorhq.com/locations/${locId}`, {
|
|
16264
|
+
method: "GET",
|
|
16265
|
+
headers: {
|
|
16266
|
+
Authorization: `Bearer ${client.getApiKey()}`,
|
|
16267
|
+
"Content-Type": "application/json",
|
|
16268
|
+
Accept: "application/json",
|
|
16269
|
+
Version: "2021-07-28"
|
|
16270
|
+
},
|
|
16271
|
+
signal: AbortSignal.timeout(8e3)
|
|
16272
|
+
});
|
|
16273
|
+
if (response.status === 401) {
|
|
16274
|
+
return { name: "GHL API key", status: "fail", detail: `401 Unauthorized \u2014 key ${client.getApiKeyPrefix()} is invalid or revoked. Run setup_ghl_mcp with a fresh key.` };
|
|
16275
|
+
}
|
|
16276
|
+
if (response.status === 403) {
|
|
16277
|
+
return { name: "GHL API key", status: "fail", detail: `403 Forbidden \u2014 key ${client.getApiKeyPrefix()} doesn't have access to location ${locId}. The key may belong to a different sub-account.` };
|
|
16278
|
+
}
|
|
16279
|
+
if (!response.ok) {
|
|
16280
|
+
return { name: "GHL API key", status: "warn", detail: `HTTP ${response.status} reading /locations/${locId} \u2014 key may be limited or GHL is having issues.` };
|
|
16281
|
+
}
|
|
16282
|
+
return { name: "GHL API key", status: "pass", detail: `Valid (key ${client.getApiKeyPrefix()}).` };
|
|
16283
|
+
} catch (error) {
|
|
16284
|
+
return { name: "GHL API key", status: "warn", detail: `Network error: ${error instanceof Error ? error.message : String(error)}. Server starts anyway; tool calls will report errors when invoked.` };
|
|
16285
|
+
}
|
|
16286
|
+
})();
|
|
16287
|
+
const locationPromise = (async () => {
|
|
16288
|
+
const locId = client.defaultLocationId;
|
|
16289
|
+
if (!locId) {
|
|
16290
|
+
return { name: "Default location", status: "warn", detail: "No default location set. Tools that don't accept locationId arg will fail." };
|
|
16291
|
+
}
|
|
16292
|
+
try {
|
|
16293
|
+
const result = await client.get(`/locations/${locId}`);
|
|
16294
|
+
const loc = result.location ?? result;
|
|
16295
|
+
return { name: "Default location", status: "pass", detail: `${loc?.name ?? "Unknown"} (${locId}) reachable.` };
|
|
16296
|
+
} catch (error) {
|
|
16297
|
+
return { name: "Default location", status: "fail", detail: `Could not read location ${locId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
16298
|
+
}
|
|
16299
|
+
})();
|
|
16300
|
+
const firebasePromise = (async () => {
|
|
16301
|
+
if (!builderClient) {
|
|
16302
|
+
return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured \u2014 your install is one step from finished. Run capture_firebase_interactive: a Chrome window opens, you log into GHL once, and it captures + verifies everything itself (about 60 seconds). This powers the account auditor and the Firebase-gated tools. No Chrome on this machine? auto_capture_firebase_script is the copy-paste path. Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart." };
|
|
16303
|
+
}
|
|
16304
|
+
const result = await builderClient.checkAuth();
|
|
16305
|
+
const tokenCompany = builderClient.getTokenCompanyId();
|
|
16306
|
+
const intendedCompany = builderClient.getIntendedCompanyId() ?? builderClient.getCurrentCompanyId();
|
|
16307
|
+
const companyNote = intendedCompany ? ` Active location's company: ${intendedCompany}.` : "";
|
|
16308
|
+
if (result.ok && intendedCompany && tokenCompany && intendedCompany !== tokenCompany) {
|
|
16309
|
+
return {
|
|
16310
|
+
name: "Firebase auth (workflow builder)",
|
|
16311
|
+
status: "fail",
|
|
16312
|
+
detail: `Company mismatch: the active location belongs to company ${intendedCompany}, but the Firebase token authenticates as ${tokenCompany}. Workflow-builder writes will 401 or hit the wrong account. This company's Firebase is missing or registered under the wrong ID. Run register_company_firebase with a token captured from that sub-account's GHL session (v3.27.0+ stores it under the correct internal ID automatically).`
|
|
16313
|
+
};
|
|
16314
|
+
}
|
|
16315
|
+
if (result.ok) {
|
|
16316
|
+
const idNote = tokenCompany ? ` Authenticated as company ${tokenCompany}.` : "";
|
|
16317
|
+
return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}${idNote}` };
|
|
16318
|
+
}
|
|
16319
|
+
return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.${companyNote} If you're in a client's account, register its Firebase with register_company_firebase. Otherwise re-capture your Firebase values from GHL DevTools and re-run enable_workflow_builder.` };
|
|
16320
|
+
})();
|
|
16321
|
+
const registryCheck = (() => {
|
|
16322
|
+
if (!registry2) {
|
|
16323
|
+
return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
|
|
16324
|
+
}
|
|
16325
|
+
const loadFailure = registry2.getLoadFailure();
|
|
16326
|
+
if (loadFailure) {
|
|
16327
|
+
return { name: "Token registry", status: "fail", detail: loadFailure };
|
|
16328
|
+
}
|
|
16329
|
+
const locs = registry2.listLocations();
|
|
16330
|
+
const companies = registry2.listCompanyFirebases();
|
|
16331
|
+
const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
|
|
16332
|
+
if (locs.length === 0) {
|
|
16333
|
+
return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
|
|
16334
|
+
}
|
|
16335
|
+
return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
|
|
16336
|
+
})();
|
|
16337
|
+
const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
|
|
16338
|
+
versionPromise,
|
|
16339
|
+
apiKeyPromise,
|
|
16340
|
+
locationPromise,
|
|
16341
|
+
firebasePromise
|
|
16342
|
+
]);
|
|
16343
|
+
let versionCheck;
|
|
16344
|
+
if (versionStatus.latest === null) {
|
|
16345
|
+
versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed}. Could not reach npm registry to check for updates: ${versionStatus.fetchError ?? "unknown"}.` };
|
|
16346
|
+
} else if (versionStatus.upToDate) {
|
|
16347
|
+
versionCheck = { name: "npm registry + version", status: "pass", detail: `Up to date at v${versionStatus.installed}.` };
|
|
16348
|
+
} else {
|
|
16349
|
+
versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed} but latest is v${versionStatus.latest}. Quit Claude (Cmd+Q) and reopen to upgrade.` };
|
|
16350
|
+
}
|
|
16351
|
+
const gatingCheck = (() => {
|
|
16352
|
+
const r = getGatingReport?.();
|
|
16353
|
+
if (!r) {
|
|
16354
|
+
return { name: "Tool gating", status: "skip", detail: "Gating report unavailable in this mode." };
|
|
16355
|
+
}
|
|
16356
|
+
if (!r.gatingActive) {
|
|
16357
|
+
return { name: "Tool gating", status: "pass", detail: `No gating configured \u2014 all ${r.registeredCount} tools registered. Sub-account create/delete stays OFF unless GHL_ENABLE_ACCOUNT_ADMIN=1 is set.` };
|
|
16358
|
+
}
|
|
16359
|
+
const parts = [`${r.registeredCount} of ${r.attemptedCount} tools registered.`];
|
|
16360
|
+
if (r.disabledTools.length > 0) parts.push(`Disabled tools: ${r.disabledTools.join(", ")}.`);
|
|
16361
|
+
if (r.disabledModules.length > 0) parts.push(`Disabled modules: ${r.disabledModules.join(", ")}.`);
|
|
16362
|
+
if (r.outOfBandDenied.length > 0) parts.push(`Confirmed unregistered (setup/helper tools): ${r.outOfBandDenied.join(", ")}.`);
|
|
16363
|
+
if (r.allowlistActive) parts.push(`Allowlist: modules=[${r.enabledModules.join(",") || "-"}], tools=[${r.enabledTools.join(",") || "-"}].`);
|
|
16364
|
+
parts.push(`Account admin (sub-account create/delete): ${r.accountAdminEnabled ? "ENABLED via GHL_ENABLE_ACCOUNT_ADMIN=1" : "OFF (default)"}.`);
|
|
16365
|
+
const problems = [];
|
|
16366
|
+
if (r.ignoredHardCoreDenials.length > 0) problems.push(`recovery-core tool(s) in GHL_DISABLED_TOOLS cannot be disabled and were IGNORED: ${r.ignoredHardCoreDenials.join(", ")}`);
|
|
16367
|
+
if (r.unknownDisabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_TOOLS (possible typo \u2014 those tools are NOT disabled): ${r.unknownDisabledTools.join(", ")}`);
|
|
16368
|
+
if (r.unknownDisabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_MODULES (possible typo): ${r.unknownDisabledModules.join(", ")}`);
|
|
16369
|
+
if (r.unknownEnabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_TOOLS: ${r.unknownEnabledTools.join(", ")}`);
|
|
16370
|
+
if (r.unknownEnabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_MODULES: ${r.unknownEnabledModules.join(", ")}`);
|
|
16371
|
+
if (problems.length > 0) {
|
|
16372
|
+
return { name: "Tool gating", status: "warn", detail: `${parts.join(" ")} PROBLEMS: ${problems.join("; ")}. Fix the env var in your Claude config and fully restart Claude.` };
|
|
16373
|
+
}
|
|
16374
|
+
return { name: "Tool gating", status: "pass", detail: parts.join(" ") };
|
|
16375
|
+
})();
|
|
16376
|
+
const telemetryCheck = telemetryDisabled(process.env) ? {
|
|
16377
|
+
name: "Anonymous usage stats",
|
|
16378
|
+
status: "skip",
|
|
16379
|
+
detail: `OFF \u2014 ${process.env.GHL_TELEMETRY?.trim() === "0" ? "GHL_TELEMETRY=0" : "DO_NOT_TRACK"} is set. Nothing is sent.`
|
|
16380
|
+
} : {
|
|
16381
|
+
name: "Anonymous usage stats",
|
|
16382
|
+
status: "pass",
|
|
16383
|
+
detail: `ON. ${TELEMETRY_DISCLOSURE} Sent: install/startup events, package version, Node major, platform, and a random device id. NEVER sent: your license key, email, GHL API key, location ID, hostname, file paths, or any GHL account data.`
|
|
16384
|
+
};
|
|
16385
|
+
checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck, gatingCheck, telemetryCheck);
|
|
16386
|
+
return checks;
|
|
16387
|
+
}
|
|
16388
|
+
function renderChecks(checks) {
|
|
16389
|
+
const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
|
|
16390
|
+
const lines = [];
|
|
16391
|
+
for (const c of checks) {
|
|
16392
|
+
lines.push(`${symbols[c.status]} ${c.name.padEnd(35)} ${c.status.toUpperCase()}`);
|
|
16393
|
+
lines.push(` ${c.detail}`);
|
|
16394
|
+
}
|
|
16395
|
+
return lines;
|
|
16396
|
+
}
|
|
16397
|
+
function summarizeChecks(checks) {
|
|
16398
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
16399
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
16400
|
+
if (fails === 0 && warns === 0) return "All systems go.";
|
|
16401
|
+
const parts = [];
|
|
16402
|
+
if (fails > 0) parts.push(`${fails} failure(s)`);
|
|
16403
|
+
if (warns > 0) parts.push(`${warns} warning(s)`);
|
|
16404
|
+
return parts.join(", ") + " \u2014 see details above.";
|
|
16405
|
+
}
|
|
15606
16406
|
function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
|
|
15607
16407
|
server2.tool(
|
|
15608
16408
|
"health_check",
|
|
15609
|
-
"Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), token registry, and the tool-gating policy (disabled tools/modules, account-admin opt-in, typos in gating env vars). Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change.",
|
|
16409
|
+
"Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), token registry, and the tool-gating policy (disabled tools/modules, account-admin opt-in, typos in gating env vars). Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change. For a wider report that also probes every GHL capability live and shows what you are not yet using, run `run_checkup`.",
|
|
15610
16410
|
{},
|
|
15611
16411
|
async () => {
|
|
15612
|
-
const checks =
|
|
15613
|
-
const versionPromise = getVersionStatus(installedVersion);
|
|
15614
|
-
const apiKeyPromise = (async () => {
|
|
15615
|
-
const locId = client.defaultLocationId;
|
|
15616
|
-
if (!locId) {
|
|
15617
|
-
return { name: "GHL API key", status: "warn", detail: `Can't validate without a default location ID. Set GHL_LOCATION_ID or run setup_ghl_mcp.` };
|
|
15618
|
-
}
|
|
15619
|
-
try {
|
|
15620
|
-
const response = await fetch(`https://services.leadconnectorhq.com/locations/${locId}`, {
|
|
15621
|
-
method: "GET",
|
|
15622
|
-
headers: {
|
|
15623
|
-
Authorization: `Bearer ${client.getApiKey()}`,
|
|
15624
|
-
"Content-Type": "application/json",
|
|
15625
|
-
Accept: "application/json",
|
|
15626
|
-
Version: "2021-07-28"
|
|
15627
|
-
},
|
|
15628
|
-
signal: AbortSignal.timeout(8e3)
|
|
15629
|
-
});
|
|
15630
|
-
if (response.status === 401) {
|
|
15631
|
-
return { name: "GHL API key", status: "fail", detail: `401 Unauthorized \u2014 key ${client.getApiKeyPrefix()} is invalid or revoked. Run setup_ghl_mcp with a fresh key.` };
|
|
15632
|
-
}
|
|
15633
|
-
if (response.status === 403) {
|
|
15634
|
-
return { name: "GHL API key", status: "fail", detail: `403 Forbidden \u2014 key ${client.getApiKeyPrefix()} doesn't have access to location ${locId}. The key may belong to a different sub-account.` };
|
|
15635
|
-
}
|
|
15636
|
-
if (!response.ok) {
|
|
15637
|
-
return { name: "GHL API key", status: "warn", detail: `HTTP ${response.status} reading /locations/${locId} \u2014 key may be limited or GHL is having issues.` };
|
|
15638
|
-
}
|
|
15639
|
-
return { name: "GHL API key", status: "pass", detail: `Valid (key ${client.getApiKeyPrefix()}).` };
|
|
15640
|
-
} catch (error) {
|
|
15641
|
-
return { name: "GHL API key", status: "warn", detail: `Network error: ${error instanceof Error ? error.message : String(error)}. Server starts anyway; tool calls will report errors when invoked.` };
|
|
15642
|
-
}
|
|
15643
|
-
})();
|
|
15644
|
-
const locationPromise = (async () => {
|
|
15645
|
-
const locId = client.defaultLocationId;
|
|
15646
|
-
if (!locId) {
|
|
15647
|
-
return { name: "Default location", status: "warn", detail: "No default location set. Tools that don't accept locationId arg will fail." };
|
|
15648
|
-
}
|
|
15649
|
-
try {
|
|
15650
|
-
const result = await client.get(`/locations/${locId}`);
|
|
15651
|
-
const loc = result.location ?? result;
|
|
15652
|
-
return { name: "Default location", status: "pass", detail: `${loc?.name ?? "Unknown"} (${locId}) reachable.` };
|
|
15653
|
-
} catch (error) {
|
|
15654
|
-
return { name: "Default location", status: "fail", detail: `Could not read location ${locId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
15655
|
-
}
|
|
15656
|
-
})();
|
|
15657
|
-
const firebasePromise = (async () => {
|
|
15658
|
-
if (!builderClient) {
|
|
15659
|
-
return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured \u2014 your install is one step from finished. Run capture_firebase_interactive: a Chrome window opens, you log into GHL once, and it captures + verifies everything itself (about 60 seconds). This powers the account auditor and the Firebase-gated tools. No Chrome on this machine? auto_capture_firebase_script is the copy-paste path. Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart." };
|
|
15660
|
-
}
|
|
15661
|
-
const result = await builderClient.checkAuth();
|
|
15662
|
-
const tokenCompany = builderClient.getTokenCompanyId();
|
|
15663
|
-
const intendedCompany = builderClient.getIntendedCompanyId() ?? builderClient.getCurrentCompanyId();
|
|
15664
|
-
const companyNote = intendedCompany ? ` Active location's company: ${intendedCompany}.` : "";
|
|
15665
|
-
if (result.ok && intendedCompany && tokenCompany && intendedCompany !== tokenCompany) {
|
|
15666
|
-
return {
|
|
15667
|
-
name: "Firebase auth (workflow builder)",
|
|
15668
|
-
status: "fail",
|
|
15669
|
-
detail: `Company mismatch: the active location belongs to company ${intendedCompany}, but the Firebase token authenticates as ${tokenCompany}. Workflow-builder writes will 401 or hit the wrong account. This company's Firebase is missing or registered under the wrong ID. Run register_company_firebase with a token captured from that sub-account's GHL session (v3.27.0+ stores it under the correct internal ID automatically).`
|
|
15670
|
-
};
|
|
15671
|
-
}
|
|
15672
|
-
if (result.ok) {
|
|
15673
|
-
const idNote = tokenCompany ? ` Authenticated as company ${tokenCompany}.` : "";
|
|
15674
|
-
return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}${idNote}` };
|
|
15675
|
-
}
|
|
15676
|
-
return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.${companyNote} If you're in a client's account, register its Firebase with register_company_firebase. Otherwise re-capture your Firebase values from GHL DevTools and re-run enable_workflow_builder.` };
|
|
15677
|
-
})();
|
|
15678
|
-
const registryCheck = (() => {
|
|
15679
|
-
if (!registry2) {
|
|
15680
|
-
return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
|
|
15681
|
-
}
|
|
15682
|
-
const loadFailure = registry2.getLoadFailure();
|
|
15683
|
-
if (loadFailure) {
|
|
15684
|
-
return { name: "Token registry", status: "fail", detail: loadFailure };
|
|
15685
|
-
}
|
|
15686
|
-
const locs = registry2.listLocations();
|
|
15687
|
-
const companies = registry2.listCompanyFirebases();
|
|
15688
|
-
const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
|
|
15689
|
-
if (locs.length === 0) {
|
|
15690
|
-
return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
|
|
15691
|
-
}
|
|
15692
|
-
return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
|
|
15693
|
-
})();
|
|
15694
|
-
const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
|
|
15695
|
-
versionPromise,
|
|
15696
|
-
apiKeyPromise,
|
|
15697
|
-
locationPromise,
|
|
15698
|
-
firebasePromise
|
|
15699
|
-
]);
|
|
15700
|
-
let versionCheck;
|
|
15701
|
-
if (versionStatus.latest === null) {
|
|
15702
|
-
versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed}. Could not reach npm registry to check for updates: ${versionStatus.fetchError ?? "unknown"}.` };
|
|
15703
|
-
} else if (versionStatus.upToDate) {
|
|
15704
|
-
versionCheck = { name: "npm registry + version", status: "pass", detail: `Up to date at v${versionStatus.installed}.` };
|
|
15705
|
-
} else {
|
|
15706
|
-
versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed} but latest is v${versionStatus.latest}. Quit Claude (Cmd+Q) and reopen to upgrade.` };
|
|
15707
|
-
}
|
|
15708
|
-
const gatingCheck = (() => {
|
|
15709
|
-
const r = getGatingReport?.();
|
|
15710
|
-
if (!r) {
|
|
15711
|
-
return { name: "Tool gating", status: "skip", detail: "Gating report unavailable in this mode." };
|
|
15712
|
-
}
|
|
15713
|
-
if (!r.gatingActive) {
|
|
15714
|
-
return { name: "Tool gating", status: "pass", detail: `No gating configured \u2014 all ${r.registeredCount} tools registered. Sub-account create/delete stays OFF unless GHL_ENABLE_ACCOUNT_ADMIN=1 is set.` };
|
|
15715
|
-
}
|
|
15716
|
-
const parts = [`${r.registeredCount} of ${r.attemptedCount} tools registered.`];
|
|
15717
|
-
if (r.disabledTools.length > 0) parts.push(`Disabled tools: ${r.disabledTools.join(", ")}.`);
|
|
15718
|
-
if (r.disabledModules.length > 0) parts.push(`Disabled modules: ${r.disabledModules.join(", ")}.`);
|
|
15719
|
-
if (r.outOfBandDenied.length > 0) parts.push(`Confirmed unregistered (setup/helper tools): ${r.outOfBandDenied.join(", ")}.`);
|
|
15720
|
-
if (r.allowlistActive) parts.push(`Allowlist: modules=[${r.enabledModules.join(",") || "-"}], tools=[${r.enabledTools.join(",") || "-"}].`);
|
|
15721
|
-
parts.push(`Account admin (sub-account create/delete): ${r.accountAdminEnabled ? "ENABLED via GHL_ENABLE_ACCOUNT_ADMIN=1" : "OFF (default)"}.`);
|
|
15722
|
-
const problems = [];
|
|
15723
|
-
if (r.ignoredHardCoreDenials.length > 0) problems.push(`recovery-core tool(s) in GHL_DISABLED_TOOLS cannot be disabled and were IGNORED: ${r.ignoredHardCoreDenials.join(", ")}`);
|
|
15724
|
-
if (r.unknownDisabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_TOOLS (possible typo \u2014 those tools are NOT disabled): ${r.unknownDisabledTools.join(", ")}`);
|
|
15725
|
-
if (r.unknownDisabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_MODULES (possible typo): ${r.unknownDisabledModules.join(", ")}`);
|
|
15726
|
-
if (r.unknownEnabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_TOOLS: ${r.unknownEnabledTools.join(", ")}`);
|
|
15727
|
-
if (r.unknownEnabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_MODULES: ${r.unknownEnabledModules.join(", ")}`);
|
|
15728
|
-
if (problems.length > 0) {
|
|
15729
|
-
return { name: "Tool gating", status: "warn", detail: `${parts.join(" ")} PROBLEMS: ${problems.join("; ")}. Fix the env var in your Claude config and fully restart Claude.` };
|
|
15730
|
-
}
|
|
15731
|
-
return { name: "Tool gating", status: "pass", detail: parts.join(" ") };
|
|
15732
|
-
})();
|
|
15733
|
-
const telemetryCheck = telemetryDisabled(process.env) ? {
|
|
15734
|
-
name: "Anonymous usage stats",
|
|
15735
|
-
status: "skip",
|
|
15736
|
-
detail: `OFF \u2014 ${process.env.GHL_TELEMETRY?.trim() === "0" ? "GHL_TELEMETRY=0" : "DO_NOT_TRACK"} is set. Nothing is sent.`
|
|
15737
|
-
} : {
|
|
15738
|
-
name: "Anonymous usage stats",
|
|
15739
|
-
status: "pass",
|
|
15740
|
-
detail: `ON. ${TELEMETRY_DISCLOSURE} Sent: install/startup events, package version, Node major, platform, and a random device id. NEVER sent: your license key, email, GHL API key, location ID, hostname, file paths, or any GHL account data.`
|
|
15741
|
-
};
|
|
15742
|
-
checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck, gatingCheck, telemetryCheck);
|
|
15743
|
-
const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
|
|
16412
|
+
const checks = await collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport);
|
|
15744
16413
|
const lines = [];
|
|
15745
16414
|
lines.push("GHL Command \u2014 Health Check");
|
|
15746
16415
|
lines.push("\u2501".repeat(50));
|
|
15747
|
-
|
|
15748
|
-
lines.push(`${symbols[c.status]} ${c.name.padEnd(35)} ${c.status.toUpperCase()}`);
|
|
15749
|
-
lines.push(` ${c.detail}`);
|
|
15750
|
-
}
|
|
16416
|
+
lines.push(...renderChecks(checks));
|
|
15751
16417
|
lines.push("\u2501".repeat(50));
|
|
15752
|
-
|
|
15753
|
-
const warns = checks.filter((c) => c.status === "warn").length;
|
|
15754
|
-
if (fails === 0 && warns === 0) {
|
|
15755
|
-
lines.push("All systems go.");
|
|
15756
|
-
} else {
|
|
15757
|
-
const parts = [];
|
|
15758
|
-
if (fails > 0) parts.push(`${fails} failure(s)`);
|
|
15759
|
-
if (warns > 0) parts.push(`${warns} warning(s)`);
|
|
15760
|
-
lines.push(parts.join(", ") + " \u2014 see details above.");
|
|
15761
|
-
}
|
|
16418
|
+
lines.push(summarizeChecks(checks));
|
|
15762
16419
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
15763
16420
|
}
|
|
15764
16421
|
);
|
|
15765
16422
|
}
|
|
15766
16423
|
|
|
16424
|
+
// src/tools/checkup.ts
|
|
16425
|
+
var import_zod54 = require("zod");
|
|
16426
|
+
var PROBES = [
|
|
16427
|
+
{ key: "contacts", label: "Contacts", path: "/contacts/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["contacts"] },
|
|
16428
|
+
{ key: "opportunities", label: "Opportunities", path: "/opportunities/search", params: { location_id: "{loc}", limit: 1 }, listKeys: ["opportunities"] },
|
|
16429
|
+
{ key: "pipelines", label: "Pipelines", path: "/opportunities/pipelines", params: { locationId: "{loc}" }, listKeys: ["pipelines"] },
|
|
16430
|
+
{ key: "conversations", label: "Conversations", path: "/conversations/search", params: { locationId: "{loc}", limit: 1 }, listKeys: ["conversations"] },
|
|
16431
|
+
{ key: "calendars", label: "Calendars", path: "/calendars/", params: { locationId: "{loc}" }, listKeys: ["calendars"] },
|
|
16432
|
+
{ key: "workflows", label: "Workflows", path: "/workflows/", params: { locationId: "{loc}" }, listKeys: ["workflows"] },
|
|
16433
|
+
{ key: "funnels", label: "Funnels and websites", path: "/funnels/funnel/list", params: { locationId: "{loc}", limit: 1 }, listKeys: ["funnels"] },
|
|
16434
|
+
{ key: "forms", label: "Forms", path: "/forms/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["forms"] },
|
|
16435
|
+
{ key: "surveys", label: "Surveys", path: "/surveys/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["surveys"] },
|
|
16436
|
+
{ key: "customFields", label: "Custom fields", path: "/locations/{loc}/customFields", listKeys: ["customFields"] },
|
|
16437
|
+
{ key: "customValues", label: "Custom values", path: "/locations/{loc}/customValues", listKeys: ["customValues"] },
|
|
16438
|
+
{ key: "tags", label: "Tags", path: "/locations/{loc}/tags", listKeys: ["tags"] },
|
|
16439
|
+
{ key: "users", label: "Users", path: "/users/", params: { locationId: "{loc}" }, listKeys: ["users"] },
|
|
16440
|
+
{ key: "customObjects", label: "Custom objects", path: "/objects/", params: { locationId: "{loc}" }, listKeys: ["objects"], optional: true },
|
|
16441
|
+
{ key: "triggerLinks", label: "Trigger links", path: "/links/", params: { locationId: "{loc}" }, listKeys: ["links"] },
|
|
16442
|
+
{ key: "products", label: "Products", path: "/products/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["products"] },
|
|
16443
|
+
// limit AND offset are required by GHL here — omitting either 422s. Same
|
|
16444
|
+
// params the shipped list_invoices sends (live-verified 2026-08-08).
|
|
16445
|
+
{ key: "invoices", label: "Invoices", path: "/invoices/", params: { locationId: "{loc}", altId: "{loc}", altType: "location", limit: 1, offset: 0 }, listKeys: ["invoices"] },
|
|
16446
|
+
{ key: "media", label: "Media library", path: "/medias/files", params: { altId: "{loc}", altType: "location", type: "file", limit: 1 }, listKeys: ["files"] },
|
|
16447
|
+
{ key: "phoneNumbers", label: "Phone numbers", path: "/phone-system/numbers/", params: { locationId: "{loc}" }, listKeys: ["phoneNumbers"] },
|
|
16448
|
+
{ key: "socialAccounts", label: "Social accounts", path: "/social-media-posting/{loc}/accounts", listKeys: ["accounts"], optional: true },
|
|
16449
|
+
// limit/offset required here too, for the same reason as invoices.
|
|
16450
|
+
{ key: "blogAuthors", label: "Blogs", path: "/blogs/authors", params: { locationId: "{loc}", limit: 50, offset: 0 }, listKeys: ["authors"], optional: true },
|
|
16451
|
+
{ key: "campaigns", label: "Campaigns", path: "/campaigns/", params: { locationId: "{loc}" }, listKeys: ["campaigns"], optional: true },
|
|
16452
|
+
{ key: "courses", label: "Courses and memberships", path: "/courses/", params: { locationId: "{loc}" }, optional: true },
|
|
16453
|
+
{ key: "businesses", label: "Businesses", path: "/businesses/", params: { locationId: "{loc}" }, listKeys: ["businesses"], optional: true }
|
|
16454
|
+
];
|
|
16455
|
+
var BUILDER_PROBE_KEY = "workflowBuilder";
|
|
16456
|
+
function parseStatus(message) {
|
|
16457
|
+
const m = /GHL API Error (\d{3})/.exec(message);
|
|
16458
|
+
return m ? Number(m[1]) : null;
|
|
16459
|
+
}
|
|
16460
|
+
function classifyFailure(status, optional) {
|
|
16461
|
+
if (status === null) {
|
|
16462
|
+
return { verdict: "inconclusive", detail: "Could not reach GHL (network or timeout). Says nothing about this feature \u2014 run the checkup again." };
|
|
16463
|
+
}
|
|
16464
|
+
if (status === 401) {
|
|
16465
|
+
return { verdict: "no-access", detail: "401 \u2014 the API key was rejected. See the Connection section above." };
|
|
16466
|
+
}
|
|
16467
|
+
if (status === 403) {
|
|
16468
|
+
return { verdict: "no-access", detail: "403 \u2014 this API key's scopes don't include this feature. Fix: edit the Private Integration in GHL and tick every scope, then re-run setup." };
|
|
16469
|
+
}
|
|
16470
|
+
if (status === 429) {
|
|
16471
|
+
return { verdict: "inconclusive", detail: "429 \u2014 GHL rate-limited this probe. Not a fault. Re-run in a minute." };
|
|
16472
|
+
}
|
|
16473
|
+
if (status >= 500) {
|
|
16474
|
+
return { verdict: "inconclusive", detail: `${status} \u2014 GHL server error. Their side, not yours. Re-run later.` };
|
|
16475
|
+
}
|
|
16476
|
+
if (optional) {
|
|
16477
|
+
return { verdict: "unavailable", detail: `${status} \u2014 not readable on this account. Usually means the feature isn't enabled on this GHL plan.` };
|
|
16478
|
+
}
|
|
16479
|
+
return { verdict: "broken", detail: `${status} \u2014 GHL rejected a read that should work. This is what a GoHighLevel change looks like. Please send this report to support@ghlcommand.com.` };
|
|
16480
|
+
}
|
|
16481
|
+
function countRows(raw, listKeys) {
|
|
16482
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
16483
|
+
const obj = raw;
|
|
16484
|
+
const meta = obj.meta;
|
|
16485
|
+
if (typeof meta === "object" && meta !== null) {
|
|
16486
|
+
const total = meta.total;
|
|
16487
|
+
if (typeof total === "number") return total;
|
|
16488
|
+
}
|
|
16489
|
+
if (typeof obj.total === "number") return obj.total;
|
|
16490
|
+
for (const key of listKeys ?? []) {
|
|
16491
|
+
if (Array.isArray(obj[key])) return obj[key].length;
|
|
16492
|
+
}
|
|
16493
|
+
for (const value of Object.values(obj)) {
|
|
16494
|
+
if (Array.isArray(value)) return value.length;
|
|
16495
|
+
if (typeof value === "object" && value !== null) {
|
|
16496
|
+
for (const inner of Object.values(value)) {
|
|
16497
|
+
if (Array.isArray(inner)) return inner.length;
|
|
16498
|
+
}
|
|
16499
|
+
}
|
|
16500
|
+
}
|
|
16501
|
+
return null;
|
|
16502
|
+
}
|
|
16503
|
+
function substitute(value, loc) {
|
|
16504
|
+
return typeof value === "string" ? value.replace("{loc}", loc) : value;
|
|
16505
|
+
}
|
|
16506
|
+
var CONFIRM_ATTEMPTS = 3;
|
|
16507
|
+
var CONFIRM_DELAY_MS = 600;
|
|
16508
|
+
async function confirm(run) {
|
|
16509
|
+
let last = await run();
|
|
16510
|
+
for (let attempt = 1; attempt < CONFIRM_ATTEMPTS && last.verdict !== "working" && last.verdict !== "skipped"; attempt++) {
|
|
16511
|
+
await new Promise((r) => setTimeout(r, CONFIRM_DELAY_MS));
|
|
16512
|
+
last = await run();
|
|
16513
|
+
}
|
|
16514
|
+
if (last.verdict === "working" || last.verdict === "skipped") return last;
|
|
16515
|
+
return { ...last, detail: `${last.detail} (Confirmed on ${CONFIRM_ATTEMPTS} separate reads, so this is not a blip.)` };
|
|
16516
|
+
}
|
|
16517
|
+
async function runProbes(client, loc) {
|
|
16518
|
+
return Promise.all(
|
|
16519
|
+
PROBES.map((probe) => confirm(async () => {
|
|
16520
|
+
const path15 = probe.path.replace("{loc}", loc);
|
|
16521
|
+
const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
|
|
16522
|
+
try {
|
|
16523
|
+
const raw = await client.get(path15, { params, noRetry: true });
|
|
16524
|
+
const count = countRows(raw, probe.listKeys);
|
|
16525
|
+
return {
|
|
16526
|
+
key: probe.key,
|
|
16527
|
+
label: probe.label,
|
|
16528
|
+
verdict: "working",
|
|
16529
|
+
count,
|
|
16530
|
+
status: 200,
|
|
16531
|
+
detail: count === null ? "Reads fine." : count === 0 ? "Reads fine. You have none yet." : `Reads fine. ${count} in this account.`
|
|
16532
|
+
};
|
|
16533
|
+
} catch (error) {
|
|
16534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16535
|
+
const status = parseStatus(message);
|
|
16536
|
+
const { verdict, detail } = classifyFailure(status, probe.optional === true);
|
|
16537
|
+
return { key: probe.key, label: probe.label, verdict, count: null, status, detail };
|
|
16538
|
+
}
|
|
16539
|
+
}))
|
|
16540
|
+
);
|
|
16541
|
+
}
|
|
16542
|
+
var BUILDER_AUTH_FAILURE = /TOKEN_EXPIRED|INVALID_ARGUMENT|UNAUTHENTICATED|token refresh|refresh token|invalid[_ ]grant|unauthorized/i;
|
|
16543
|
+
async function runBuilderProbe(builderClient) {
|
|
16544
|
+
return confirm(() => builderProbeOnce(builderClient));
|
|
16545
|
+
}
|
|
16546
|
+
async function builderProbeOnce(builderClient) {
|
|
16547
|
+
const base = { key: BUILDER_PROBE_KEY, label: "Workflow Builder (build + edit)", count: null, status: null };
|
|
16548
|
+
if (!builderClient) {
|
|
16549
|
+
return { ...base, verdict: "skipped", detail: "Not connected. This is step 2 of the install, not an extra. Say: Unlock the Workflow Builder." };
|
|
16550
|
+
}
|
|
16551
|
+
try {
|
|
16552
|
+
const raw = await builderClient.listWorkflows(1, 0);
|
|
16553
|
+
const count = countRows(raw, ["workflows"]);
|
|
16554
|
+
return { ...base, verdict: "working", status: 200, count, detail: "Connected. Claude can build and edit workflows in this account." };
|
|
16555
|
+
} catch (error) {
|
|
16556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16557
|
+
const status = parseStatus(message);
|
|
16558
|
+
if (status === 401 || status === 403 || BUILDER_AUTH_FAILURE.test(message)) {
|
|
16559
|
+
return {
|
|
16560
|
+
...base,
|
|
16561
|
+
status,
|
|
16562
|
+
verdict: "no-access",
|
|
16563
|
+
detail: "The saved GHL browser login has expired. Say: Unlock the Workflow Builder. It takes about 60 seconds and needs no pasting."
|
|
16564
|
+
};
|
|
16565
|
+
}
|
|
16566
|
+
if (status === null || status === 429 || status >= 500) {
|
|
16567
|
+
return { ...base, status, verdict: "inconclusive", detail: `Could not complete the check${status ? ` (${status})` : ""}. Says nothing either way. Run the checkup again.` };
|
|
16568
|
+
}
|
|
16569
|
+
return { ...base, status, verdict: "broken", detail: `${status} \u2014 the internal API rejected a read that should work. Please send this report to support@ghlcommand.com.` };
|
|
16570
|
+
}
|
|
16571
|
+
}
|
|
16572
|
+
function buildNudges(probes, builder, registeredLocations) {
|
|
16573
|
+
const by = new Map(probes.map((p) => [p.key, p]));
|
|
16574
|
+
const count = (key) => by.get(key)?.count ?? null;
|
|
16575
|
+
const emptyAndWorking = (key) => by.get(key)?.verdict === "working" && by.get(key)?.count === 0;
|
|
16576
|
+
const nudges = [];
|
|
16577
|
+
if (builder.verdict === "skipped" || builder.verdict === "no-access") {
|
|
16578
|
+
nudges.push({
|
|
16579
|
+
headline: "The Workflow Builder is off \u2014 that is the half that BUILDS.",
|
|
16580
|
+
prompt: "Unlock the Workflow Builder.",
|
|
16581
|
+
guide: "install-ghl-command"
|
|
16582
|
+
});
|
|
16583
|
+
}
|
|
16584
|
+
const workflows = count("workflows");
|
|
16585
|
+
if (workflows !== null && workflows > 0) {
|
|
16586
|
+
nudges.push({
|
|
16587
|
+
// Deliberately not "you have never audited these" — nothing here can
|
|
16588
|
+
// know that, and a checkup that overclaims loses the reader.
|
|
16589
|
+
headline: `${workflows} workflow(s) here. A silent-failure audit finds what GHL never warns you about.`,
|
|
16590
|
+
prompt: "Audit my workflows and find broken pipeline, stage, or field IDs.",
|
|
16591
|
+
guide: "audit-your-workflows"
|
|
16592
|
+
});
|
|
16593
|
+
} else if (emptyAndWorking("workflows")) {
|
|
16594
|
+
nudges.push({
|
|
16595
|
+
headline: "No workflows in this account \u2014 nothing follows up on its own.",
|
|
16596
|
+
prompt: "Build a 7-day nurture sequence with texts, emails, and waits that stops the moment they reply.",
|
|
16597
|
+
guide: "build-nurture-sequence"
|
|
16598
|
+
});
|
|
16599
|
+
}
|
|
16600
|
+
const contacts = count("contacts");
|
|
16601
|
+
if (emptyAndWorking("pipelines") && contacts !== null && contacts > 0) {
|
|
16602
|
+
nudges.push({
|
|
16603
|
+
headline: `${contacts} contacts and no pipeline \u2014 nothing tracks where they are.`,
|
|
16604
|
+
prompt: "Stand up a New Patient pipeline with stages for Lead, Booked, Showed, Treated, and Rebooked.",
|
|
16605
|
+
guide: "stand-up-a-pipeline"
|
|
16606
|
+
});
|
|
16607
|
+
}
|
|
16608
|
+
if (emptyAndWorking("calendars")) {
|
|
16609
|
+
nudges.push({
|
|
16610
|
+
headline: "No booking calendar \u2014 every appointment is being set by hand.",
|
|
16611
|
+
prompt: "Set up a booking calendar for me and show me the booking link.",
|
|
16612
|
+
guide: "set-up-a-booking-calendar"
|
|
16613
|
+
});
|
|
16614
|
+
}
|
|
16615
|
+
if (emptyAndWorking("forms")) {
|
|
16616
|
+
nudges.push({
|
|
16617
|
+
headline: "No forms \u2014 there is no way for a lead to raise their hand.",
|
|
16618
|
+
prompt: "Create an intake form for new leads and show me where the submissions land.",
|
|
16619
|
+
guide: "create-forms-and-read-submissions"
|
|
16620
|
+
});
|
|
16621
|
+
}
|
|
16622
|
+
if (emptyAndWorking("triggerLinks")) {
|
|
16623
|
+
nudges.push({
|
|
16624
|
+
headline: "No trigger links \u2014 you cannot see who clicked what.",
|
|
16625
|
+
prompt: "Create a trigger link and a workflow that tags anyone who clicks it.",
|
|
16626
|
+
guide: "trigger-links"
|
|
16627
|
+
});
|
|
16628
|
+
}
|
|
16629
|
+
if (emptyAndWorking("customValues")) {
|
|
16630
|
+
nudges.push({
|
|
16631
|
+
headline: "No custom values \u2014 AI bots and templates have nothing to personalize from.",
|
|
16632
|
+
prompt: "Set up the custom values this account needs so my templates and AI bot stop hard-coding details.",
|
|
16633
|
+
guide: "ai-bots-and-custom-values"
|
|
16634
|
+
});
|
|
16635
|
+
}
|
|
16636
|
+
if (emptyAndWorking("socialAccounts")) {
|
|
16637
|
+
nudges.push({
|
|
16638
|
+
headline: "No social accounts connected \u2014 the Social Planner is unused.",
|
|
16639
|
+
prompt: "Show me what I need to connect to post to social from here.",
|
|
16640
|
+
guide: "social-posting"
|
|
16641
|
+
});
|
|
16642
|
+
}
|
|
16643
|
+
if (emptyAndWorking("products") && emptyAndWorking("invoices")) {
|
|
16644
|
+
nudges.push({
|
|
16645
|
+
headline: "No products and no invoices \u2014 nothing here can take money.",
|
|
16646
|
+
prompt: "Set up a product and send me a test invoice so I can see the payment flow.",
|
|
16647
|
+
guide: "invoices-and-payments"
|
|
16648
|
+
});
|
|
16649
|
+
}
|
|
16650
|
+
if (registeredLocations <= 1) {
|
|
16651
|
+
nudges.push({
|
|
16652
|
+
headline: "Only one sub-account is registered \u2014 Claude can work across all of them.",
|
|
16653
|
+
prompt: "Register my other sub-accounts so you can switch between them by name.",
|
|
16654
|
+
guide: "switch-between-client-accounts"
|
|
16655
|
+
});
|
|
16656
|
+
}
|
|
16657
|
+
return nudges;
|
|
16658
|
+
}
|
|
16659
|
+
var VERDICT_SYMBOL = {
|
|
16660
|
+
working: "\u2713",
|
|
16661
|
+
broken: "\u2717",
|
|
16662
|
+
"no-access": "!",
|
|
16663
|
+
unavailable: "\u2014",
|
|
16664
|
+
inconclusive: "?",
|
|
16665
|
+
skipped: "\u2014"
|
|
16666
|
+
};
|
|
16667
|
+
function renderCheckup(data) {
|
|
16668
|
+
const lines = [];
|
|
16669
|
+
lines.push("GHL Command \u2014 Checkup");
|
|
16670
|
+
lines.push(`v${data.version} \xB7 location ${data.locationId} \xB7 ${data.generatedAt}`);
|
|
16671
|
+
lines.push("\u2501".repeat(58));
|
|
16672
|
+
lines.push("");
|
|
16673
|
+
lines.push("1. CONNECTION \u2014 is the install wired up?");
|
|
16674
|
+
lines.push("\u2500".repeat(58));
|
|
16675
|
+
lines.push(...renderChecks(data.install));
|
|
16676
|
+
lines.push(summarizeChecks(data.install));
|
|
16677
|
+
lines.push("");
|
|
16678
|
+
lines.push("2. WHAT STILL WORKS \u2014 every capability read live, just now");
|
|
16679
|
+
lines.push("\u2500".repeat(58));
|
|
16680
|
+
for (const p of data.probes) {
|
|
16681
|
+
lines.push(`${VERDICT_SYMBOL[p.verdict]} ${p.label.padEnd(30)} ${p.verdict.toUpperCase()}`);
|
|
16682
|
+
lines.push(` ${p.detail}`);
|
|
16683
|
+
}
|
|
16684
|
+
const broken = data.probes.filter((p) => p.verdict === "broken");
|
|
16685
|
+
lines.push("");
|
|
16686
|
+
if (broken.length === 0) {
|
|
16687
|
+
lines.push("No breakage found. Every capability GHL Command claims still answers.");
|
|
16688
|
+
} else {
|
|
16689
|
+
lines.push(`\u26A0 ${broken.length} capability area(s) are BROKEN: ${broken.map((p) => p.label).join(", ")}.`);
|
|
16690
|
+
lines.push(" This is almost always a GoHighLevel change, not something you did.");
|
|
16691
|
+
lines.push(" Send this whole report to support@ghlcommand.com \u2014 it is exactly what we need to fix it.");
|
|
16692
|
+
}
|
|
16693
|
+
lines.push("");
|
|
16694
|
+
lines.push("3. WHAT YOU ARE NOT USING YET");
|
|
16695
|
+
lines.push("\u2500".repeat(58));
|
|
16696
|
+
lines.push(`Actively using ${data.inUse} of ${data.usable} capability areas in this account.`);
|
|
16697
|
+
lines.push("(Measured from what this GHL account contains \u2014 nothing about your usage is collected or sent.)");
|
|
16698
|
+
lines.push("");
|
|
16699
|
+
if (data.nudges.length === 0) {
|
|
16700
|
+
lines.push("Nothing obvious left on the table. This account is being worked hard.");
|
|
16701
|
+
} else {
|
|
16702
|
+
data.nudges.forEach((n, i) => {
|
|
16703
|
+
lines.push(`${i + 1}. ${n.headline}`);
|
|
16704
|
+
lines.push(` Say: "${n.prompt}"`);
|
|
16705
|
+
if (n.guide) lines.push(` Guide: ${n.guide} \u2014 open the full library with "open the user guide".`);
|
|
16706
|
+
});
|
|
16707
|
+
}
|
|
16708
|
+
lines.push("");
|
|
16709
|
+
lines.push("\u2501".repeat(58));
|
|
16710
|
+
lines.push("Run this monthly, and any time GHL feels off. Questions: support@ghlcommand.com");
|
|
16711
|
+
return lines.join("\n");
|
|
16712
|
+
}
|
|
16713
|
+
function registerCheckupTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
|
|
16714
|
+
server2.tool(
|
|
16715
|
+
"run_checkup",
|
|
16716
|
+
`The full GHL Command checkup \u2014 the answer to "am I using it all, and is it all still working?". Three sections: (1) CONNECTION, the same install checks as health_check; (2) WHAT STILL WORKS, one live read-only probe against every GHL capability family, which distinguishes "working but you own none" from "GoHighLevel changed something and this is broken"; (3) WHAT YOU ARE NOT USING YET, derived from what the account actually contains, each line carrying the exact sentence to paste back. Read-only and safe on live client accounts. Use format:'json' for a machine-readable report. Run it monthly, after a GHL update, or any time something feels off.`,
|
|
16717
|
+
{
|
|
16718
|
+
locationId: import_zod54.z.string().optional().describe("Sub-account to check. Defaults to the active location."),
|
|
16719
|
+
format: import_zod54.z.enum(["report", "json"]).optional().describe("'report' (default) is the plain-English checkup. 'json' returns the same data for scripts and support tickets.")
|
|
16720
|
+
},
|
|
16721
|
+
async ({ locationId: locationId2, format }) => {
|
|
16722
|
+
let loc;
|
|
16723
|
+
try {
|
|
16724
|
+
loc = client.resolveLocationId(locationId2);
|
|
16725
|
+
} catch (error) {
|
|
16726
|
+
return {
|
|
16727
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
16728
|
+
isError: true
|
|
16729
|
+
};
|
|
16730
|
+
}
|
|
16731
|
+
const [install, probes, builder] = await Promise.all([
|
|
16732
|
+
collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport),
|
|
16733
|
+
runProbes(client, loc),
|
|
16734
|
+
runBuilderProbe(builderClient)
|
|
16735
|
+
]);
|
|
16736
|
+
const allProbes = [...probes, builder];
|
|
16737
|
+
const usable = allProbes.filter((p) => p.verdict === "working").length;
|
|
16738
|
+
const inUse = allProbes.filter((p) => p.verdict === "working" && (p.count === null || p.count > 0)).length;
|
|
16739
|
+
const data = {
|
|
16740
|
+
version: installedVersion,
|
|
16741
|
+
locationId: loc,
|
|
16742
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16743
|
+
install,
|
|
16744
|
+
probes: allProbes,
|
|
16745
|
+
nudges: buildNudges(probes, builder, registry2?.listLocations().length ?? 0),
|
|
16746
|
+
inUse,
|
|
16747
|
+
usable
|
|
16748
|
+
};
|
|
16749
|
+
const text = format === "json" ? JSON.stringify(data, null, 2) : renderCheckup(data);
|
|
16750
|
+
return { content: [{ type: "text", text }] };
|
|
16751
|
+
}
|
|
16752
|
+
);
|
|
16753
|
+
}
|
|
16754
|
+
|
|
15767
16755
|
// src/tools/user-guide.ts
|
|
15768
16756
|
var import_child_process2 = require("child_process");
|
|
15769
16757
|
var fs8 = __toESM(require("fs"));
|
|
@@ -15816,11 +16804,11 @@ ${guidePath}`;
|
|
|
15816
16804
|
}
|
|
15817
16805
|
|
|
15818
16806
|
// src/tools/snapshots.ts
|
|
15819
|
-
var
|
|
16807
|
+
var import_zod55 = require("zod");
|
|
15820
16808
|
init_ghl_client();
|
|
15821
|
-
var SnapshotSchema =
|
|
15822
|
-
var SnapshotsResponseSchema =
|
|
15823
|
-
var ShareLinkResponseSchema =
|
|
16809
|
+
var SnapshotSchema = import_zod55.z.object({ id: import_zod55.z.string(), name: import_zod55.z.string(), type: import_zod55.z.string() }).passthrough();
|
|
16810
|
+
var SnapshotsResponseSchema = import_zod55.z.object({ snapshots: import_zod55.z.array(SnapshotSchema) }).passthrough();
|
|
16811
|
+
var ShareLinkResponseSchema = import_zod55.z.object({ id: import_zod55.z.string(), shareLink: import_zod55.z.string() }).passthrough();
|
|
15824
16812
|
var SHARE_TYPES = [
|
|
15825
16813
|
"link",
|
|
15826
16814
|
"permanent_link",
|
|
@@ -15867,7 +16855,7 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
15867
16855
|
"list_snapshots",
|
|
15868
16856
|
"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.",
|
|
15869
16857
|
{
|
|
15870
|
-
companyId:
|
|
16858
|
+
companyId: import_zod55.z.string().optional().describe(
|
|
15871
16859
|
"Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
15872
16860
|
)
|
|
15873
16861
|
},
|
|
@@ -15892,11 +16880,11 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
15892
16880
|
"create_snapshot_share_link",
|
|
15893
16881
|
"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.",
|
|
15894
16882
|
{
|
|
15895
|
-
snapshot_id:
|
|
15896
|
-
share_type:
|
|
16883
|
+
snapshot_id: import_zod55.z.string().describe("The snapshot id to share (from list_snapshots)."),
|
|
16884
|
+
share_type: import_zod55.z.enum(SHARE_TYPES).describe(
|
|
15897
16885
|
"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."
|
|
15898
16886
|
),
|
|
15899
|
-
companyId:
|
|
16887
|
+
companyId: import_zod55.z.string().optional().describe(
|
|
15900
16888
|
"Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
15901
16889
|
)
|
|
15902
16890
|
},
|
|
@@ -15926,17 +16914,17 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
15926
16914
|
}
|
|
15927
16915
|
|
|
15928
16916
|
// src/tools/phone.ts
|
|
15929
|
-
var
|
|
15930
|
-
var PhoneNumberSchema =
|
|
15931
|
-
var NumbersResponseSchema =
|
|
15932
|
-
var PoolsResponseSchema =
|
|
16917
|
+
var import_zod56 = require("zod");
|
|
16918
|
+
var PhoneNumberSchema = import_zod56.z.object({ sid: import_zod56.z.string(), value: import_zod56.z.string(), title: import_zod56.z.string().optional() }).passthrough();
|
|
16919
|
+
var NumbersResponseSchema = import_zod56.z.object({ phoneNumbers: import_zod56.z.array(PhoneNumberSchema) }).passthrough();
|
|
16920
|
+
var PoolsResponseSchema = import_zod56.z.object({ pools: import_zod56.z.array(import_zod56.z.object({}).passthrough()) }).passthrough();
|
|
15933
16921
|
function registerPhoneTools(server2, client) {
|
|
15934
16922
|
safeTool(
|
|
15935
16923
|
server2,
|
|
15936
16924
|
"list_phone_numbers",
|
|
15937
16925
|
"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).",
|
|
15938
16926
|
{
|
|
15939
|
-
locationId:
|
|
16927
|
+
locationId: import_zod56.z.string().optional().describe("Defaults to the active location.")
|
|
15940
16928
|
},
|
|
15941
16929
|
async ({ locationId: locationId2 }) => {
|
|
15942
16930
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -15954,7 +16942,7 @@ function registerPhoneTools(server2, client) {
|
|
|
15954
16942
|
"list_number_pools",
|
|
15955
16943
|
"List LC Phone number pools configured for a location. Read-only.",
|
|
15956
16944
|
{
|
|
15957
|
-
locationId:
|
|
16945
|
+
locationId: import_zod56.z.string().optional().describe("Defaults to the active location.")
|
|
15958
16946
|
},
|
|
15959
16947
|
async ({ locationId: locationId2 }) => {
|
|
15960
16948
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -15966,10 +16954,10 @@ function registerPhoneTools(server2, client) {
|
|
|
15966
16954
|
}
|
|
15967
16955
|
|
|
15968
16956
|
// src/tools/account-health.ts
|
|
15969
|
-
var
|
|
15970
|
-
var MetaTotalSchema =
|
|
15971
|
-
var TotalSchema2 =
|
|
15972
|
-
var NumbersSchema =
|
|
16957
|
+
var import_zod57 = require("zod");
|
|
16958
|
+
var MetaTotalSchema = import_zod57.z.object({ meta: import_zod57.z.object({ total: import_zod57.z.number() }).passthrough() }).passthrough();
|
|
16959
|
+
var TotalSchema2 = import_zod57.z.object({ total: import_zod57.z.number() }).passthrough();
|
|
16960
|
+
var NumbersSchema = import_zod57.z.object({ phoneNumbers: import_zod57.z.array(import_zod57.z.unknown()) }).passthrough();
|
|
15973
16961
|
var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
|
|
15974
16962
|
async function section(scope, fn) {
|
|
15975
16963
|
try {
|
|
@@ -15984,8 +16972,8 @@ function registerAccountHealthTools(server2, client) {
|
|
|
15984
16972
|
"get_account_health_summary",
|
|
15985
16973
|
"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).",
|
|
15986
16974
|
{
|
|
15987
|
-
locationId:
|
|
15988
|
-
windowDays:
|
|
16975
|
+
locationId: import_zod57.z.string().optional().describe("Defaults to the active location."),
|
|
16976
|
+
windowDays: import_zod57.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
|
|
15989
16977
|
},
|
|
15990
16978
|
async ({ locationId: locationId2, windowDays }) => {
|
|
15991
16979
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -16051,7 +17039,7 @@ function registerAccountHealthTools(server2, client) {
|
|
|
16051
17039
|
}
|
|
16052
17040
|
|
|
16053
17041
|
// src/tools/intake-to-build.ts
|
|
16054
|
-
var
|
|
17042
|
+
var import_zod60 = require("zod");
|
|
16055
17043
|
|
|
16056
17044
|
// src/intake-to-build/form-template.ts
|
|
16057
17045
|
init_question_set();
|
|
@@ -16335,7 +17323,7 @@ init_question_set();
|
|
|
16335
17323
|
init_question_set();
|
|
16336
17324
|
|
|
16337
17325
|
// src/intake-to-build/brief.ts
|
|
16338
|
-
var
|
|
17326
|
+
var import_zod58 = require("zod");
|
|
16339
17327
|
var BRIEF_SCHEMA_VERSION = "0.1";
|
|
16340
17328
|
var PRESETS = [
|
|
16341
17329
|
"generic",
|
|
@@ -16345,67 +17333,67 @@ var PRESETS = [
|
|
|
16345
17333
|
"ecom",
|
|
16346
17334
|
"agency"
|
|
16347
17335
|
];
|
|
16348
|
-
var presetSchema =
|
|
17336
|
+
var presetSchema = import_zod58.z.enum(PRESETS);
|
|
16349
17337
|
var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
|
|
16350
|
-
var briefSourceSchema =
|
|
16351
|
-
var pricePointSchema =
|
|
16352
|
-
name:
|
|
16353
|
-
price:
|
|
17338
|
+
var briefSourceSchema = import_zod58.z.enum(BRIEF_SOURCES);
|
|
17339
|
+
var pricePointSchema = import_zod58.z.object({
|
|
17340
|
+
name: import_zod58.z.string(),
|
|
17341
|
+
price: import_zod58.z.string()
|
|
16354
17342
|
});
|
|
16355
|
-
var briefSchema =
|
|
16356
|
-
schemaVersion:
|
|
16357
|
-
briefId:
|
|
17343
|
+
var briefSchema = import_zod58.z.object({
|
|
17344
|
+
schemaVersion: import_zod58.z.string(),
|
|
17345
|
+
briefId: import_zod58.z.string(),
|
|
16358
17346
|
preset: presetSchema,
|
|
16359
17347
|
briefSource: briefSourceSchema,
|
|
16360
17348
|
/** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
|
|
16361
|
-
extended:
|
|
16362
|
-
business:
|
|
16363
|
-
name:
|
|
16364
|
-
type:
|
|
16365
|
-
website:
|
|
16366
|
-
location:
|
|
16367
|
-
timezone:
|
|
17349
|
+
extended: import_zod58.z.record(import_zod58.z.unknown()).optional(),
|
|
17350
|
+
business: import_zod58.z.object({
|
|
17351
|
+
name: import_zod58.z.string(),
|
|
17352
|
+
type: import_zod58.z.string().optional(),
|
|
17353
|
+
website: import_zod58.z.string().optional(),
|
|
17354
|
+
location: import_zod58.z.string().optional(),
|
|
17355
|
+
timezone: import_zod58.z.string().optional(),
|
|
16368
17356
|
// Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
|
|
16369
17357
|
// the same tolerance reason as business.type (don't reject valid briefs).
|
|
16370
|
-
teamSize:
|
|
16371
|
-
monthlyLeadVolume:
|
|
16372
|
-
hours:
|
|
17358
|
+
teamSize: import_zod58.z.string().optional(),
|
|
17359
|
+
monthlyLeadVolume: import_zod58.z.string().optional(),
|
|
17360
|
+
hours: import_zod58.z.string().optional()
|
|
16373
17361
|
}).passthrough(),
|
|
16374
|
-
offer:
|
|
16375
|
-
summary:
|
|
17362
|
+
offer: import_zod58.z.object({
|
|
17363
|
+
summary: import_zod58.z.string().optional(),
|
|
16376
17364
|
// Parsed best-effort; tolerate a raw string when parsing was not possible.
|
|
16377
|
-
pricePoints:
|
|
16378
|
-
leadMagnet:
|
|
16379
|
-
avgDealValue:
|
|
17365
|
+
pricePoints: import_zod58.z.union([import_zod58.z.array(pricePointSchema), import_zod58.z.string()]).optional(),
|
|
17366
|
+
leadMagnet: import_zod58.z.string().optional(),
|
|
17367
|
+
avgDealValue: import_zod58.z.string().optional()
|
|
16380
17368
|
}).passthrough().optional(),
|
|
16381
|
-
audience:
|
|
16382
|
-
ideal:
|
|
16383
|
-
painPoints:
|
|
16384
|
-
objections:
|
|
17369
|
+
audience: import_zod58.z.object({
|
|
17370
|
+
ideal: import_zod58.z.string().optional(),
|
|
17371
|
+
painPoints: import_zod58.z.array(import_zod58.z.string()).optional(),
|
|
17372
|
+
objections: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
16385
17373
|
}).passthrough().optional(),
|
|
16386
|
-
goal:
|
|
17374
|
+
goal: import_zod58.z.object({
|
|
16387
17375
|
// Kept as string: the form option labels are the canonical values, but
|
|
16388
17376
|
// the contract sample shortens some (e.g. "high-touch"). See §7 note.
|
|
16389
|
-
primary:
|
|
16390
|
-
salesStages:
|
|
16391
|
-
bookingNeeded:
|
|
16392
|
-
followUpStyle:
|
|
17377
|
+
primary: import_zod58.z.string(),
|
|
17378
|
+
salesStages: import_zod58.z.array(import_zod58.z.string()).optional(),
|
|
17379
|
+
bookingNeeded: import_zod58.z.boolean().optional(),
|
|
17380
|
+
followUpStyle: import_zod58.z.string().optional()
|
|
16393
17381
|
}).passthrough(),
|
|
16394
|
-
channels:
|
|
16395
|
-
email:
|
|
16396
|
-
sms:
|
|
16397
|
-
a2pStatus:
|
|
16398
|
-
payment:
|
|
16399
|
-
calendarConnected:
|
|
16400
|
-
social:
|
|
17382
|
+
channels: import_zod58.z.object({
|
|
17383
|
+
email: import_zod58.z.boolean().optional(),
|
|
17384
|
+
sms: import_zod58.z.boolean().optional(),
|
|
17385
|
+
a2pStatus: import_zod58.z.string().optional(),
|
|
17386
|
+
payment: import_zod58.z.string().optional(),
|
|
17387
|
+
calendarConnected: import_zod58.z.boolean().optional(),
|
|
17388
|
+
social: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
16401
17389
|
}).passthrough().optional(),
|
|
16402
|
-
assets:
|
|
16403
|
-
existingPipeline:
|
|
16404
|
-
existingWorkflows:
|
|
16405
|
-
brand:
|
|
16406
|
-
notes:
|
|
17390
|
+
assets: import_zod58.z.object({
|
|
17391
|
+
existingPipeline: import_zod58.z.string().optional(),
|
|
17392
|
+
existingWorkflows: import_zod58.z.string().optional(),
|
|
17393
|
+
brand: import_zod58.z.string().optional(),
|
|
17394
|
+
notes: import_zod58.z.string().optional()
|
|
16407
17395
|
}).passthrough().optional(),
|
|
16408
|
-
flags:
|
|
17396
|
+
flags: import_zod58.z.array(import_zod58.z.string()).optional()
|
|
16409
17397
|
}).strict();
|
|
16410
17398
|
function validateBrief(input) {
|
|
16411
17399
|
const parsed = briefSchema.safeParse(input);
|
|
@@ -16563,7 +17551,7 @@ function normalizeSubmissionToBrief(opts) {
|
|
|
16563
17551
|
}
|
|
16564
17552
|
|
|
16565
17553
|
// src/intake-to-build/plan.ts
|
|
16566
|
-
var
|
|
17554
|
+
var import_zod59 = require("zod");
|
|
16567
17555
|
var REF_NAMESPACES = [
|
|
16568
17556
|
"pipeline",
|
|
16569
17557
|
"stage",
|
|
@@ -16580,22 +17568,22 @@ var REF_NAMESPACES = [
|
|
|
16580
17568
|
"handoff"
|
|
16581
17569
|
];
|
|
16582
17570
|
var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
|
|
16583
|
-
var refSchema =
|
|
17571
|
+
var refSchema = import_zod59.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
|
|
16584
17572
|
function nsRef(ns) {
|
|
16585
|
-
return
|
|
17573
|
+
return import_zod59.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
|
|
16586
17574
|
}
|
|
16587
17575
|
function refNamespace(ref) {
|
|
16588
17576
|
return ref.split(".")[0];
|
|
16589
17577
|
}
|
|
16590
|
-
var stageSchema =
|
|
17578
|
+
var stageSchema = import_zod59.z.object({
|
|
16591
17579
|
ref: nsRef("stage"),
|
|
16592
|
-
name:
|
|
16593
|
-
position:
|
|
17580
|
+
name: import_zod59.z.string(),
|
|
17581
|
+
position: import_zod59.z.number().int().nonnegative()
|
|
16594
17582
|
});
|
|
16595
|
-
var pipelineSchema =
|
|
17583
|
+
var pipelineSchema = import_zod59.z.object({
|
|
16596
17584
|
ref: nsRef("pipeline"),
|
|
16597
|
-
name:
|
|
16598
|
-
stages:
|
|
17585
|
+
name: import_zod59.z.string(),
|
|
17586
|
+
stages: import_zod59.z.array(stageSchema).min(1)
|
|
16599
17587
|
});
|
|
16600
17588
|
var GHL_FIELD_DATATYPES = [
|
|
16601
17589
|
"TEXT",
|
|
@@ -16612,21 +17600,21 @@ var GHL_FIELD_DATATYPES = [
|
|
|
16612
17600
|
"FILE_UPLOAD",
|
|
16613
17601
|
"SIGNATURE"
|
|
16614
17602
|
];
|
|
16615
|
-
var customFieldSchema =
|
|
17603
|
+
var customFieldSchema = import_zod59.z.object({
|
|
16616
17604
|
ref: nsRef("field"),
|
|
16617
|
-
name:
|
|
16618
|
-
dataType:
|
|
16619
|
-
model:
|
|
16620
|
-
options:
|
|
17605
|
+
name: import_zod59.z.string(),
|
|
17606
|
+
dataType: import_zod59.z.enum(GHL_FIELD_DATATYPES),
|
|
17607
|
+
model: import_zod59.z.enum(["contact", "opportunity"]).optional(),
|
|
17608
|
+
options: import_zod59.z.array(import_zod59.z.string()).optional()
|
|
16621
17609
|
});
|
|
16622
|
-
var tagSchema =
|
|
17610
|
+
var tagSchema = import_zod59.z.object({
|
|
16623
17611
|
ref: nsRef("tag"),
|
|
16624
|
-
name:
|
|
17612
|
+
name: import_zod59.z.string()
|
|
16625
17613
|
});
|
|
16626
|
-
var customValueSchema =
|
|
17614
|
+
var customValueSchema = import_zod59.z.object({
|
|
16627
17615
|
ref: nsRef("cv"),
|
|
16628
|
-
name:
|
|
16629
|
-
value:
|
|
17616
|
+
name: import_zod59.z.string(),
|
|
17617
|
+
value: import_zod59.z.string().optional(),
|
|
16630
17618
|
filledBy: refSchema.optional()
|
|
16631
17619
|
});
|
|
16632
17620
|
var CALENDAR_TYPES = [
|
|
@@ -16636,89 +17624,89 @@ var CALENDAR_TYPES = [
|
|
|
16636
17624
|
"collective",
|
|
16637
17625
|
"service_booking"
|
|
16638
17626
|
];
|
|
16639
|
-
var openHoursBlockSchema =
|
|
16640
|
-
daysOfTheWeek:
|
|
16641
|
-
hours:
|
|
16642
|
-
|
|
16643
|
-
openHour:
|
|
16644
|
-
openMinute:
|
|
16645
|
-
closeHour:
|
|
16646
|
-
closeMinute:
|
|
17627
|
+
var openHoursBlockSchema = import_zod59.z.object({
|
|
17628
|
+
daysOfTheWeek: import_zod59.z.array(import_zod59.z.number().int().min(0).max(6)),
|
|
17629
|
+
hours: import_zod59.z.array(
|
|
17630
|
+
import_zod59.z.object({
|
|
17631
|
+
openHour: import_zod59.z.number().int().min(0).max(23),
|
|
17632
|
+
openMinute: import_zod59.z.number().int().min(0).max(59),
|
|
17633
|
+
closeHour: import_zod59.z.number().int().min(0).max(23),
|
|
17634
|
+
closeMinute: import_zod59.z.number().int().min(0).max(59)
|
|
16647
17635
|
})
|
|
16648
17636
|
)
|
|
16649
17637
|
});
|
|
16650
|
-
var calendarSchema =
|
|
17638
|
+
var calendarSchema = import_zod59.z.object({
|
|
16651
17639
|
ref: nsRef("calendar"),
|
|
16652
|
-
name:
|
|
16653
|
-
calendarType:
|
|
16654
|
-
openHours:
|
|
16655
|
-
availabilityType:
|
|
16656
|
-
requiresStaff:
|
|
17640
|
+
name: import_zod59.z.string(),
|
|
17641
|
+
calendarType: import_zod59.z.enum(CALENDAR_TYPES),
|
|
17642
|
+
openHours: import_zod59.z.array(openHoursBlockSchema).optional(),
|
|
17643
|
+
availabilityType: import_zod59.z.number().int().optional(),
|
|
17644
|
+
requiresStaff: import_zod59.z.boolean().optional()
|
|
16657
17645
|
});
|
|
16658
|
-
var formFieldSchema =
|
|
16659
|
-
|
|
16660
|
-
type:
|
|
16661
|
-
key:
|
|
16662
|
-
required:
|
|
17646
|
+
var formFieldSchema = import_zod59.z.discriminatedUnion("type", [
|
|
17647
|
+
import_zod59.z.object({
|
|
17648
|
+
type: import_zod59.z.literal("standard"),
|
|
17649
|
+
key: import_zod59.z.string(),
|
|
17650
|
+
required: import_zod59.z.boolean().optional()
|
|
16663
17651
|
}),
|
|
16664
|
-
|
|
16665
|
-
type:
|
|
17652
|
+
import_zod59.z.object({
|
|
17653
|
+
type: import_zod59.z.literal("custom"),
|
|
16666
17654
|
fieldRef: nsRef("field"),
|
|
16667
|
-
required:
|
|
17655
|
+
required: import_zod59.z.boolean().optional()
|
|
16668
17656
|
})
|
|
16669
17657
|
]);
|
|
16670
|
-
var formSchema =
|
|
17658
|
+
var formSchema = import_zod59.z.object({
|
|
16671
17659
|
ref: nsRef("form"),
|
|
16672
|
-
name:
|
|
16673
|
-
fields:
|
|
17660
|
+
name: import_zod59.z.string(),
|
|
17661
|
+
fields: import_zod59.z.array(formFieldSchema)
|
|
16674
17662
|
});
|
|
16675
|
-
var pageSchema =
|
|
17663
|
+
var pageSchema = import_zod59.z.object({
|
|
16676
17664
|
ref: nsRef("page"),
|
|
16677
|
-
name:
|
|
16678
|
-
role:
|
|
16679
|
-
outline:
|
|
17665
|
+
name: import_zod59.z.string(),
|
|
17666
|
+
role: import_zod59.z.string().optional(),
|
|
17667
|
+
outline: import_zod59.z.string().optional(),
|
|
16680
17668
|
formRef: nsRef("form").optional(),
|
|
16681
17669
|
calendarRef: nsRef("calendar").optional()
|
|
16682
17670
|
});
|
|
16683
17671
|
var FUNNEL_TARGETS = ["ghl", "external"];
|
|
16684
17672
|
var FUNNEL_HOSTS = ["cloudflare", "vercel"];
|
|
16685
|
-
var funnelSchema =
|
|
17673
|
+
var funnelSchema = import_zod59.z.object({
|
|
16686
17674
|
ref: nsRef("funnel"),
|
|
16687
|
-
name:
|
|
17675
|
+
name: import_zod59.z.string(),
|
|
16688
17676
|
// Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
|
|
16689
17677
|
// "external" = the subscriber builds + hosts the site themselves (Cloudflare/
|
|
16690
17678
|
// Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
|
|
16691
17679
|
// see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
|
|
16692
17680
|
// deploy an external funnel; it surfaces the GHL-side wiring info.
|
|
16693
|
-
target:
|
|
16694
|
-
host:
|
|
17681
|
+
target: import_zod59.z.enum(FUNNEL_TARGETS).optional(),
|
|
17682
|
+
host: import_zod59.z.enum(FUNNEL_HOSTS).optional(),
|
|
16695
17683
|
// external only
|
|
16696
|
-
domain:
|
|
17684
|
+
domain: import_zod59.z.string().optional(),
|
|
16697
17685
|
// external only
|
|
16698
|
-
pages:
|
|
17686
|
+
pages: import_zod59.z.array(pageSchema)
|
|
16699
17687
|
});
|
|
16700
|
-
var emailAssetSchema =
|
|
17688
|
+
var emailAssetSchema = import_zod59.z.object({
|
|
16701
17689
|
ref: nsRef("email"),
|
|
16702
|
-
name:
|
|
16703
|
-
subject:
|
|
16704
|
-
bodyOutline:
|
|
16705
|
-
body:
|
|
16706
|
-
mergeTags:
|
|
17690
|
+
name: import_zod59.z.string(),
|
|
17691
|
+
subject: import_zod59.z.string().optional(),
|
|
17692
|
+
bodyOutline: import_zod59.z.string().optional(),
|
|
17693
|
+
body: import_zod59.z.string().optional(),
|
|
17694
|
+
mergeTags: import_zod59.z.array(import_zod59.z.string()).optional()
|
|
16707
17695
|
});
|
|
16708
|
-
var smsAssetSchema =
|
|
17696
|
+
var smsAssetSchema = import_zod59.z.object({
|
|
16709
17697
|
ref: nsRef("sms"),
|
|
16710
|
-
name:
|
|
16711
|
-
bodyOutline:
|
|
16712
|
-
body:
|
|
16713
|
-
mergeTags:
|
|
17698
|
+
name: import_zod59.z.string(),
|
|
17699
|
+
bodyOutline: import_zod59.z.string().optional(),
|
|
17700
|
+
body: import_zod59.z.string().optional(),
|
|
17701
|
+
mergeTags: import_zod59.z.array(import_zod59.z.string()).optional()
|
|
16714
17702
|
});
|
|
16715
|
-
var waitUnit =
|
|
17703
|
+
var waitUnit = import_zod59.z.enum(["minutes", "hours", "days"]);
|
|
16716
17704
|
var branchActionOptions = [
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
17705
|
+
import_zod59.z.object({ type: import_zod59.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
|
|
17706
|
+
import_zod59.z.object({ type: import_zod59.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
|
|
17707
|
+
import_zod59.z.object({ type: import_zod59.z.literal("send_email"), emailRef: nsRef("email") }),
|
|
17708
|
+
import_zod59.z.object({ type: import_zod59.z.literal("send_sms"), smsRef: nsRef("sms") }),
|
|
17709
|
+
import_zod59.z.object({ type: import_zod59.z.literal("wait"), value: import_zod59.z.number().positive(), unit: waitUnit }),
|
|
16722
17710
|
// Appointment-relative wait ("wait until N BEFORE the appointment").
|
|
16723
17711
|
// Only works when the workflow has an appointment in context (i.e. an
|
|
16724
17712
|
// `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
|
|
@@ -16727,80 +17715,80 @@ var branchActionOptions = [
|
|
|
16727
17715
|
// (GHL stores whole minutes). Only "before" is emitted today — that's the
|
|
16728
17716
|
// shape we captured + proved; "after" (post-appointment follow-up) is
|
|
16729
17717
|
// deferred until its shape is captured from a real workflow.
|
|
16730
|
-
|
|
16731
|
-
type:
|
|
16732
|
-
value:
|
|
17718
|
+
import_zod59.z.object({
|
|
17719
|
+
type: import_zod59.z.literal("wait_appointment"),
|
|
17720
|
+
value: import_zod59.z.number().int().positive(),
|
|
16733
17721
|
unit: waitUnit
|
|
16734
17722
|
}),
|
|
16735
|
-
|
|
16736
|
-
type:
|
|
16737
|
-
to:
|
|
16738
|
-
title:
|
|
16739
|
-
body:
|
|
17723
|
+
import_zod59.z.object({
|
|
17724
|
+
type: import_zod59.z.literal("internal_notification"),
|
|
17725
|
+
to: import_zod59.z.string(),
|
|
17726
|
+
title: import_zod59.z.string(),
|
|
17727
|
+
body: import_zod59.z.string()
|
|
16740
17728
|
}),
|
|
16741
|
-
|
|
16742
|
-
type:
|
|
17729
|
+
import_zod59.z.object({
|
|
17730
|
+
type: import_zod59.z.literal("update_contact_field"),
|
|
16743
17731
|
fieldRef: nsRef("field"),
|
|
16744
|
-
value:
|
|
17732
|
+
value: import_zod59.z.string()
|
|
16745
17733
|
}),
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
type:
|
|
16749
|
-
title:
|
|
16750
|
-
body:
|
|
16751
|
-
dueDate:
|
|
16752
|
-
assignedTo:
|
|
17734
|
+
import_zod59.z.object({ type: import_zod59.z.literal("add_notes"), body: import_zod59.z.string() }),
|
|
17735
|
+
import_zod59.z.object({
|
|
17736
|
+
type: import_zod59.z.literal("task_notification"),
|
|
17737
|
+
title: import_zod59.z.string(),
|
|
17738
|
+
body: import_zod59.z.string().optional(),
|
|
17739
|
+
dueDate: import_zod59.z.string().optional(),
|
|
17740
|
+
assignedTo: import_zod59.z.string().optional()
|
|
16753
17741
|
}),
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
type:
|
|
17742
|
+
import_zod59.z.object({ type: import_zod59.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
|
|
17743
|
+
import_zod59.z.object({ type: import_zod59.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
|
|
17744
|
+
import_zod59.z.object({
|
|
17745
|
+
type: import_zod59.z.literal("create_opportunity"),
|
|
16758
17746
|
pipelineRef: nsRef("pipeline"),
|
|
16759
17747
|
stageRef: nsRef("stage"),
|
|
16760
17748
|
// Opportunity name (merge fields allowed). Defaults to the contact's name.
|
|
16761
17749
|
// Required by GHL's create node; without it the create silently no-ops.
|
|
16762
|
-
name:
|
|
17750
|
+
name: import_zod59.z.string().optional(),
|
|
16763
17751
|
// Opportunity monetary value (the deal/sale dollar amount). A string so it
|
|
16764
17752
|
// can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
|
|
16765
17753
|
// Optional — omitted → GHL leaves the value unset. Shape captured from Lux
|
|
16766
17754
|
// Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
|
|
16767
17755
|
// won/lost status, so a "won" opp = move to the closing stage WITH this value.
|
|
16768
|
-
value:
|
|
17756
|
+
value: import_zod59.z.string().optional()
|
|
16769
17757
|
}),
|
|
16770
|
-
|
|
16771
|
-
type:
|
|
17758
|
+
import_zod59.z.object({
|
|
17759
|
+
type: import_zod59.z.literal("update_opportunity"),
|
|
16772
17760
|
pipelineRef: nsRef("pipeline"),
|
|
16773
17761
|
stageRef: nsRef("stage"),
|
|
16774
17762
|
// Opportunity monetary value (see create_opportunity.value). Optional.
|
|
16775
|
-
value:
|
|
17763
|
+
value: import_zod59.z.string().optional()
|
|
16776
17764
|
}),
|
|
16777
|
-
|
|
16778
|
-
type:
|
|
16779
|
-
goalCondition:
|
|
17765
|
+
import_zod59.z.object({
|
|
17766
|
+
type: import_zod59.z.literal("goal_event"),
|
|
17767
|
+
goalCondition: import_zod59.z.string(),
|
|
16780
17768
|
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
16781
|
-
action:
|
|
17769
|
+
action: import_zod59.z.enum(["exit", "continue", "wait"]).optional()
|
|
16782
17770
|
})
|
|
16783
17771
|
];
|
|
16784
|
-
var branchActionSchema =
|
|
16785
|
-
var findOpportunitySchema =
|
|
16786
|
-
type:
|
|
17772
|
+
var branchActionSchema = import_zod59.z.discriminatedUnion("type", branchActionOptions);
|
|
17773
|
+
var findOpportunitySchema = import_zod59.z.object({
|
|
17774
|
+
type: import_zod59.z.literal("find_opportunity"),
|
|
16787
17775
|
pipelineRef: nsRef("pipeline"),
|
|
16788
|
-
found:
|
|
16789
|
-
notFound:
|
|
17776
|
+
found: import_zod59.z.array(branchActionSchema).default([]),
|
|
17777
|
+
notFound: import_zod59.z.array(branchActionSchema).default([])
|
|
16790
17778
|
});
|
|
16791
|
-
var actionSchema =
|
|
17779
|
+
var actionSchema = import_zod59.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
16792
17780
|
var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
16793
17781
|
var CALL_STATUSES = ["busy", "canceled", "voicemail", "no-answer", "completed"];
|
|
16794
17782
|
var NUMBER_VALIDATION_STATES = ["not_valid", "sms_incapable"];
|
|
16795
|
-
var triggerSchema =
|
|
16796
|
-
type:
|
|
17783
|
+
var triggerSchema = import_zod59.z.object({
|
|
17784
|
+
type: import_zod59.z.string(),
|
|
16797
17785
|
formRef: nsRef("form").optional(),
|
|
16798
17786
|
tagRef: nsRef("tag").optional(),
|
|
16799
17787
|
calendarRef: nsRef("calendar").optional(),
|
|
16800
17788
|
pipelineRef: nsRef("pipeline").optional(),
|
|
16801
17789
|
stageRef: nsRef("stage").optional(),
|
|
16802
17790
|
// Required for a native `appointment` trigger (the status it fires on).
|
|
16803
|
-
appointmentStatus:
|
|
17791
|
+
appointmentStatus: import_zod59.z.enum(APPOINTMENT_STATUSES).optional(),
|
|
16804
17792
|
// ── customer_reply scoping ──────────────────────────────────────
|
|
16805
17793
|
// A bare customer_reply trigger fires on EVERY inbound reply from EVERY
|
|
16806
17794
|
// contact. That is almost never what a plan means: "alert the owner when a
|
|
@@ -16814,27 +17802,27 @@ var triggerSchema = import_zod58.z.object({
|
|
|
16814
17802
|
// Both optional and additive — omitting them keeps the long-standing
|
|
16815
17803
|
// fires-on-any-reply baseline.
|
|
16816
17804
|
hasTagRef: nsRef("tag").optional(),
|
|
16817
|
-
replyIntent:
|
|
17805
|
+
replyIntent: import_zod59.z.enum(["positive", "negative"]).optional(),
|
|
16818
17806
|
// ── call_status scoping (missed-call text-back) ─────────────────
|
|
16819
17807
|
// GHL's call_status trigger fires on a completed call attempt. The states it
|
|
16820
17808
|
// exposes are exactly what a "missed call" means operationally. Required for
|
|
16821
17809
|
// a call_status trigger — without them the trigger would fire on EVERY call
|
|
16822
17810
|
// including answered ones, so the executor refuses rather than guess.
|
|
16823
|
-
callStatuses:
|
|
16824
|
-
callDirection:
|
|
17811
|
+
callStatuses: import_zod59.z.array(import_zod59.z.enum(CALL_STATUSES)).optional(),
|
|
17812
|
+
callDirection: import_zod59.z.enum(["inbound", "outbound"]).optional(),
|
|
16825
17813
|
// ── validation_error (GHL UI: "Number validation") ──────────────
|
|
16826
17814
|
// Fires after a phone number passes or fails a validation check. The UI shows
|
|
16827
17815
|
// "Not valid" / "SMS incapable"; the WIRE values are snake_case. Required for
|
|
16828
17816
|
// a validation_error trigger — without them the executor refuses rather than
|
|
16829
17817
|
// guess which failure states to fire on.
|
|
16830
|
-
numberValidation:
|
|
17818
|
+
numberValidation: import_zod59.z.array(import_zod59.z.enum(NUMBER_VALIDATION_STATES)).optional()
|
|
16831
17819
|
});
|
|
16832
|
-
var workflowSchema =
|
|
17820
|
+
var workflowSchema = import_zod59.z.object({
|
|
16833
17821
|
ref: nsRef("workflow"),
|
|
16834
|
-
name:
|
|
17822
|
+
name: import_zod59.z.string(),
|
|
16835
17823
|
trigger: triggerSchema.optional(),
|
|
16836
|
-
stopOnResponse:
|
|
16837
|
-
actions:
|
|
17824
|
+
stopOnResponse: import_zod59.z.boolean().optional(),
|
|
17825
|
+
actions: import_zod59.z.array(actionSchema).max(40)
|
|
16838
17826
|
// house rule: <=40 actions/workflow
|
|
16839
17827
|
});
|
|
16840
17828
|
var HANDOFF_OWNER_LEGACY = {
|
|
@@ -16842,35 +17830,35 @@ var HANDOFF_OWNER_LEGACY = {
|
|
|
16842
17830
|
"JERRY-EXT": "OPERATOR-EXT",
|
|
16843
17831
|
"SASHA": "TEAM"
|
|
16844
17832
|
};
|
|
16845
|
-
var handoffSchema =
|
|
17833
|
+
var handoffSchema = import_zod59.z.object({
|
|
16846
17834
|
ref: nsRef("handoff"),
|
|
16847
|
-
owner:
|
|
16848
|
-
title:
|
|
16849
|
-
trigger:
|
|
16850
|
-
instruction:
|
|
17835
|
+
owner: import_zod59.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
|
|
17836
|
+
title: import_zod59.z.string(),
|
|
17837
|
+
trigger: import_zod59.z.string().optional(),
|
|
17838
|
+
instruction: import_zod59.z.string(),
|
|
16851
17839
|
produces: refSchema.nullable().optional(),
|
|
16852
|
-
successCheck:
|
|
16853
|
-
blocks:
|
|
17840
|
+
successCheck: import_zod59.z.string(),
|
|
17841
|
+
blocks: import_zod59.z.array(import_zod59.z.string()).optional()
|
|
16854
17842
|
});
|
|
16855
|
-
var buildPlanSchema =
|
|
16856
|
-
schemaVersion:
|
|
16857
|
-
planId:
|
|
16858
|
-
briefId:
|
|
16859
|
-
preset:
|
|
16860
|
-
summary:
|
|
16861
|
-
pipelines:
|
|
16862
|
-
customFields:
|
|
16863
|
-
tags:
|
|
16864
|
-
customValues:
|
|
16865
|
-
calendars:
|
|
16866
|
-
forms:
|
|
16867
|
-
funnels:
|
|
16868
|
-
emails:
|
|
16869
|
-
sms:
|
|
16870
|
-
workflows:
|
|
16871
|
-
handoffs:
|
|
16872
|
-
buildOrder:
|
|
16873
|
-
idMap:
|
|
17843
|
+
var buildPlanSchema = import_zod59.z.object({
|
|
17844
|
+
schemaVersion: import_zod59.z.string(),
|
|
17845
|
+
planId: import_zod59.z.string(),
|
|
17846
|
+
briefId: import_zod59.z.string(),
|
|
17847
|
+
preset: import_zod59.z.string(),
|
|
17848
|
+
summary: import_zod59.z.string().optional(),
|
|
17849
|
+
pipelines: import_zod59.z.array(pipelineSchema).optional(),
|
|
17850
|
+
customFields: import_zod59.z.array(customFieldSchema).optional(),
|
|
17851
|
+
tags: import_zod59.z.array(tagSchema).optional(),
|
|
17852
|
+
customValues: import_zod59.z.array(customValueSchema).optional(),
|
|
17853
|
+
calendars: import_zod59.z.array(calendarSchema).optional(),
|
|
17854
|
+
forms: import_zod59.z.array(formSchema).optional(),
|
|
17855
|
+
funnels: import_zod59.z.array(funnelSchema).optional(),
|
|
17856
|
+
emails: import_zod59.z.array(emailAssetSchema).optional(),
|
|
17857
|
+
sms: import_zod59.z.array(smsAssetSchema).optional(),
|
|
17858
|
+
workflows: import_zod59.z.array(workflowSchema).optional(),
|
|
17859
|
+
handoffs: import_zod59.z.array(handoffSchema).optional(),
|
|
17860
|
+
buildOrder: import_zod59.z.array(import_zod59.z.string()).optional(),
|
|
17861
|
+
idMap: import_zod59.z.record(import_zod59.z.string()).optional()
|
|
16874
17862
|
}).strict();
|
|
16875
17863
|
function collectDefinedRefs(plan) {
|
|
16876
17864
|
const refs = /* @__PURE__ */ new Map();
|
|
@@ -18540,16 +19528,16 @@ function msg(e) {
|
|
|
18540
19528
|
}
|
|
18541
19529
|
|
|
18542
19530
|
// src/tools/intake-to-build.ts
|
|
18543
|
-
var customFieldItemSchema =
|
|
18544
|
-
id:
|
|
18545
|
-
name:
|
|
18546
|
-
fieldKey:
|
|
18547
|
-
dataType:
|
|
18548
|
-
model:
|
|
18549
|
-
parentId:
|
|
18550
|
-
position:
|
|
18551
|
-
dateAdded:
|
|
18552
|
-
picklistOptions:
|
|
19531
|
+
var customFieldItemSchema = import_zod60.z.object({
|
|
19532
|
+
id: import_zod60.z.string(),
|
|
19533
|
+
name: import_zod60.z.string(),
|
|
19534
|
+
fieldKey: import_zod60.z.string(),
|
|
19535
|
+
dataType: import_zod60.z.string(),
|
|
19536
|
+
model: import_zod60.z.string().optional(),
|
|
19537
|
+
parentId: import_zod60.z.string().optional(),
|
|
19538
|
+
position: import_zod60.z.number().optional(),
|
|
19539
|
+
dateAdded: import_zod60.z.string().optional(),
|
|
19540
|
+
picklistOptions: import_zod60.z.array(import_zod60.z.string()).optional()
|
|
18553
19541
|
}).passthrough();
|
|
18554
19542
|
function parseCustomFields(raw) {
|
|
18555
19543
|
const obj = raw && typeof raw === "object" ? raw : {};
|
|
@@ -18958,7 +19946,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
18958
19946
|
"validate_brief",
|
|
18959
19947
|
"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.",
|
|
18960
19948
|
{
|
|
18961
|
-
brief:
|
|
19949
|
+
brief: import_zod60.z.record(import_zod60.z.unknown()).describe("The Brief object to validate.")
|
|
18962
19950
|
},
|
|
18963
19951
|
async ({ brief }) => validateBrief(brief)
|
|
18964
19952
|
);
|
|
@@ -18967,7 +19955,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
18967
19955
|
"validate_build_plan",
|
|
18968
19956
|
"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}.",
|
|
18969
19957
|
{
|
|
18970
|
-
plan:
|
|
19958
|
+
plan: import_zod60.z.record(import_zod60.z.unknown()).describe("The Build Plan object to validate.")
|
|
18971
19959
|
},
|
|
18972
19960
|
async ({ plan }) => validateBuildPlan(plan)
|
|
18973
19961
|
);
|
|
@@ -18975,12 +19963,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
18975
19963
|
"apply_build_plan",
|
|
18976
19964
|
`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.`,
|
|
18977
19965
|
{
|
|
18978
|
-
plan:
|
|
18979
|
-
mode:
|
|
18980
|
-
locationId:
|
|
18981
|
-
metHandoffs:
|
|
18982
|
-
publishWorkflows:
|
|
18983
|
-
onConflict:
|
|
19966
|
+
plan: import_zod60.z.record(import_zod60.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
19967
|
+
mode: import_zod60.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
19968
|
+
locationId: import_zod60.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
|
|
19969
|
+
metHandoffs: import_zod60.z.array(import_zod60.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.'),
|
|
19970
|
+
publishWorkflows: import_zod60.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
|
|
19971
|
+
onConflict: import_zod60.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
|
|
18984
19972
|
},
|
|
18985
19973
|
async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
|
|
18986
19974
|
try {
|
|
@@ -19183,9 +20171,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
19183
20171
|
"install_intake_form",
|
|
19184
20172
|
"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.",
|
|
19185
20173
|
{
|
|
19186
|
-
dryRun:
|
|
19187
|
-
formId:
|
|
19188
|
-
formName:
|
|
20174
|
+
dryRun: import_zod60.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
20175
|
+
formId: import_zod60.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
20176
|
+
formName: import_zod60.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
|
|
19189
20177
|
},
|
|
19190
20178
|
async ({ dryRun, formId, formName }) => {
|
|
19191
20179
|
try {
|
|
@@ -19273,10 +20261,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
19273
20261
|
"normalize_submission_to_brief",
|
|
19274
20262
|
'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).',
|
|
19275
20263
|
{
|
|
19276
|
-
formId:
|
|
19277
|
-
submissionId:
|
|
19278
|
-
fieldMap:
|
|
19279
|
-
preset:
|
|
20264
|
+
formId: import_zod60.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
20265
|
+
submissionId: import_zod60.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
20266
|
+
fieldMap: import_zod60.z.record(import_zod60.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
|
|
20267
|
+
preset: import_zod60.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
|
|
19280
20268
|
},
|
|
19281
20269
|
async ({ formId, submissionId, fieldMap, preset }) => {
|
|
19282
20270
|
try {
|
|
@@ -19305,7 +20293,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
19305
20293
|
const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
|
|
19306
20294
|
resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
|
|
19307
20295
|
}
|
|
19308
|
-
const presetSchema2 =
|
|
20296
|
+
const presetSchema2 = import_zod60.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
|
|
19309
20297
|
const presetParsed = presetSchema2.safeParse(preset);
|
|
19310
20298
|
const brief = normalizeSubmissionToBrief({
|
|
19311
20299
|
others,
|
|
@@ -19387,6 +20375,7 @@ var FORM_BUILDER_MODULE = "form-builder";
|
|
|
19387
20375
|
var INTAKE_TO_BUILD_MODULE = "intake-to-build";
|
|
19388
20376
|
var FUNNEL_QA_MODULE = "funnel-qa";
|
|
19389
20377
|
var USER_GUIDE_MODULE = "user-guide";
|
|
20378
|
+
var CHECKUP_MODULE = "checkup";
|
|
19390
20379
|
var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
19391
20380
|
...publicApiTools.map(([, label]) => label),
|
|
19392
20381
|
...internalApiTools.map(([, label]) => label),
|
|
@@ -19394,6 +20383,7 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
|
19394
20383
|
INTAKE_TO_BUILD_MODULE,
|
|
19395
20384
|
FUNNEL_QA_MODULE,
|
|
19396
20385
|
USER_GUIDE_MODULE,
|
|
20386
|
+
CHECKUP_MODULE,
|
|
19397
20387
|
VALIDATORS_MODULE,
|
|
19398
20388
|
DIAGNOSTICS_MODULE,
|
|
19399
20389
|
LOCATION_SWITCHER_MODULE,
|
|
@@ -19426,6 +20416,14 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
19426
20416
|
registry2 ?? null,
|
|
19427
20417
|
() => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
|
|
19428
20418
|
);
|
|
20419
|
+
registerCheckupTools(
|
|
20420
|
+
wrap(CHECKUP_MODULE),
|
|
20421
|
+
mcpVersion ?? "unknown",
|
|
20422
|
+
client,
|
|
20423
|
+
builderClient,
|
|
20424
|
+
registry2 ?? null,
|
|
20425
|
+
() => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
|
|
20426
|
+
);
|
|
19429
20427
|
registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
|
|
19430
20428
|
registerAccountExportTools(wrap(ACCOUNT_EXPORT_MODULE), client, registry2, builderClient);
|
|
19431
20429
|
registerLocationSwitcherTools(
|
|
@@ -19483,7 +20481,7 @@ init_credentials_store();
|
|
|
19483
20481
|
init_setup_tool();
|
|
19484
20482
|
|
|
19485
20483
|
// src/tools/skills.ts
|
|
19486
|
-
var
|
|
20484
|
+
var import_zod61 = require("zod");
|
|
19487
20485
|
|
|
19488
20486
|
// src/skill-installer.ts
|
|
19489
20487
|
var import_node_crypto2 = require("node:crypto");
|
|
@@ -19603,7 +20601,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
19603
20601
|
"install_skills",
|
|
19604
20602
|
"Install (or repair) the guided skills bundled with GHL Command \u2014 Blueprint (build a whole client account from one intake), Clone Site (clone and rebrand a live web page for a client, with a rights declaration and a pre-launch liability report), and GHL Reports (verified counts, lists, and weekly reports without token burn) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
|
|
19605
20603
|
{
|
|
19606
|
-
targetDir:
|
|
20604
|
+
targetDir: import_zod61.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
|
|
19607
20605
|
},
|
|
19608
20606
|
async ({ targetDir }) => {
|
|
19609
20607
|
try {
|