@elitedcs/ghl-mcp 3.48.2 → 3.49.1
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 +38 -0
- package/README.md +15 -6
- package/dist/capture-helper.js +288 -0
- package/dist/index.js +777 -502
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -31,15 +31,16 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "@elitedcs/ghl-mcp",
|
|
34
|
-
version: "3.
|
|
34
|
+
version: "3.49.1",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
36
|
+
description: "GoHighLevel MCP Server for Claude. 229 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
37
37
|
main: "dist/index.js",
|
|
38
38
|
bin: {
|
|
39
39
|
"ghl-mcp": "dist/index.js"
|
|
40
40
|
},
|
|
41
41
|
files: [
|
|
42
42
|
"dist/index.js",
|
|
43
|
+
"dist/capture-helper.js",
|
|
43
44
|
"templates/action-schemas.json",
|
|
44
45
|
"templates/clinic-medspa.json",
|
|
45
46
|
"templates/trigger-schemas.json",
|
|
@@ -49,7 +50,7 @@ var require_package = __commonJS({
|
|
|
49
50
|
"CHANGELOG.md"
|
|
50
51
|
],
|
|
51
52
|
scripts: {
|
|
52
|
-
build: "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external",
|
|
53
|
+
build: "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
|
|
53
54
|
setup: "node setup-wizard.mjs",
|
|
54
55
|
start: "node dist/index.js",
|
|
55
56
|
dev: "tsc --watch",
|
|
@@ -92,6 +93,7 @@ var require_package = __commonJS({
|
|
|
92
93
|
dependencies: {
|
|
93
94
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
94
95
|
dotenv: "^16.5.0",
|
|
96
|
+
"playwright-core": "^1.61.1",
|
|
95
97
|
zod: "^3.24.4"
|
|
96
98
|
},
|
|
97
99
|
devDependencies: {
|
|
@@ -108,8 +110,8 @@ var require_package = __commonJS({
|
|
|
108
110
|
|
|
109
111
|
// src/index.ts
|
|
110
112
|
var dotenv2 = __toESM(require("dotenv"));
|
|
111
|
-
var
|
|
112
|
-
var
|
|
113
|
+
var path7 = __toESM(require("path"));
|
|
114
|
+
var fs7 = __toESM(require("fs"));
|
|
113
115
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
114
116
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
115
117
|
|
|
@@ -173,8 +175,8 @@ var GHLClient = class {
|
|
|
173
175
|
Version: version || GHL_API_VERSION
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
|
-
buildUrl(
|
|
177
|
-
const url = new URL(
|
|
178
|
+
buildUrl(path8, params) {
|
|
179
|
+
const url = new URL(path8, GHL_BASE_URL);
|
|
178
180
|
if (params) {
|
|
179
181
|
for (const [key, value] of Object.entries(params)) {
|
|
180
182
|
if (value !== void 0 && value !== null) {
|
|
@@ -184,8 +186,8 @@ var GHLClient = class {
|
|
|
184
186
|
}
|
|
185
187
|
return url.toString();
|
|
186
188
|
}
|
|
187
|
-
async request(method,
|
|
188
|
-
const url = this.buildUrl(
|
|
189
|
+
async request(method, path8, options = {}, attempt = 0) {
|
|
190
|
+
const url = this.buildUrl(path8, options.params);
|
|
189
191
|
const headers = this.buildHeaders(options.version);
|
|
190
192
|
const fetchOptions = {
|
|
191
193
|
method,
|
|
@@ -203,14 +205,14 @@ var GHLClient = class {
|
|
|
203
205
|
} catch (error) {
|
|
204
206
|
clearTimeout(timeout);
|
|
205
207
|
if (error instanceof Error && error.name === "AbortError") {
|
|
206
|
-
throw new Error(`Request timeout (30s): ${method} ${
|
|
208
|
+
throw new Error(`Request timeout (30s): ${method} ${path8}`);
|
|
207
209
|
}
|
|
208
210
|
if (!options.noRetry && attempt < MAX_RETRIES) {
|
|
209
211
|
const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
|
|
210
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
212
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path8}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
211
213
|
`);
|
|
212
214
|
await new Promise((r) => setTimeout(r, delay4));
|
|
213
|
-
return this.request(method,
|
|
215
|
+
return this.request(method, path8, options, attempt + 1);
|
|
214
216
|
}
|
|
215
217
|
throw error;
|
|
216
218
|
} finally {
|
|
@@ -218,10 +220,10 @@ var GHLClient = class {
|
|
|
218
220
|
}
|
|
219
221
|
if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
220
222
|
const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
|
|
221
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
223
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path8}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
222
224
|
`);
|
|
223
225
|
await new Promise((r) => setTimeout(r, delay4));
|
|
224
|
-
return this.request(method,
|
|
226
|
+
return this.request(method, path8, options, attempt + 1);
|
|
225
227
|
}
|
|
226
228
|
if (!response.ok) {
|
|
227
229
|
let errorBody = "";
|
|
@@ -230,7 +232,7 @@ var GHLClient = class {
|
|
|
230
232
|
} catch {
|
|
231
233
|
}
|
|
232
234
|
throw new Error(
|
|
233
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
235
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path8}
|
|
234
236
|
${errorBody}`
|
|
235
237
|
);
|
|
236
238
|
}
|
|
@@ -242,20 +244,20 @@ ${errorBody}`
|
|
|
242
244
|
return { message: text };
|
|
243
245
|
}
|
|
244
246
|
}
|
|
245
|
-
async get(
|
|
246
|
-
return this.request("GET",
|
|
247
|
+
async get(path8, options) {
|
|
248
|
+
return this.request("GET", path8, options);
|
|
247
249
|
}
|
|
248
|
-
async post(
|
|
249
|
-
return this.request("POST",
|
|
250
|
+
async post(path8, options) {
|
|
251
|
+
return this.request("POST", path8, options);
|
|
250
252
|
}
|
|
251
|
-
async put(
|
|
252
|
-
return this.request("PUT",
|
|
253
|
+
async put(path8, options) {
|
|
254
|
+
return this.request("PUT", path8, options);
|
|
253
255
|
}
|
|
254
|
-
async patch(
|
|
255
|
-
return this.request("PATCH",
|
|
256
|
+
async patch(path8, options) {
|
|
257
|
+
return this.request("PATCH", path8, options);
|
|
256
258
|
}
|
|
257
|
-
async delete(
|
|
258
|
-
return this.request("DELETE",
|
|
259
|
+
async delete(path8, options) {
|
|
260
|
+
return this.request("DELETE", path8, options);
|
|
259
261
|
}
|
|
260
262
|
/**
|
|
261
263
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -4584,16 +4586,16 @@ function registerEmailTools(server2, client) {
|
|
|
4584
4586
|
function registerEmailBuilderInternalTools(server2, builderClient) {
|
|
4585
4587
|
const client = builderClient;
|
|
4586
4588
|
if (!client) return;
|
|
4587
|
-
async function builderRequest(method,
|
|
4589
|
+
async function builderRequest(method, path8, body) {
|
|
4588
4590
|
const headers = await client.buildHeaders();
|
|
4589
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
4591
|
+
const response = await fetch(`${EMAIL_BUILDER_BASE}${path8}`, {
|
|
4590
4592
|
method,
|
|
4591
4593
|
headers,
|
|
4592
4594
|
body: body ? JSON.stringify(body) : void 0
|
|
4593
4595
|
});
|
|
4594
4596
|
if (!response.ok) {
|
|
4595
4597
|
const text2 = await response.text();
|
|
4596
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
4598
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path8}
|
|
4597
4599
|
${text2}`);
|
|
4598
4600
|
}
|
|
4599
4601
|
const text = await response.text();
|
|
@@ -5850,23 +5852,23 @@ var import_zod34 = require("zod");
|
|
|
5850
5852
|
function registerFunnelBuilderTools(server2, builderClient) {
|
|
5851
5853
|
const client = builderClient;
|
|
5852
5854
|
if (!client) return;
|
|
5853
|
-
async function internalGet(
|
|
5854
|
-
return client.request("GET",
|
|
5855
|
+
async function internalGet(path8) {
|
|
5856
|
+
return client.request("GET", path8);
|
|
5855
5857
|
}
|
|
5856
|
-
async function internalPost(
|
|
5857
|
-
return client.request("POST",
|
|
5858
|
+
async function internalPost(path8, body) {
|
|
5859
|
+
return client.request("POST", path8, body);
|
|
5858
5860
|
}
|
|
5859
|
-
async function internalPut(
|
|
5860
|
-
return client.request("PUT",
|
|
5861
|
+
async function internalPut(path8, body) {
|
|
5862
|
+
return client.request("PUT", path8, body);
|
|
5861
5863
|
}
|
|
5862
|
-
async function internalDelete(
|
|
5863
|
-
return client.request("DELETE",
|
|
5864
|
+
async function internalDelete(path8) {
|
|
5865
|
+
return client.request("DELETE", path8);
|
|
5864
5866
|
}
|
|
5865
|
-
async function funnelRequest(method,
|
|
5867
|
+
async function funnelRequest(method, path8, body) {
|
|
5866
5868
|
const headers = await client.buildHeaders();
|
|
5867
5869
|
headers.Origin = "https://app.gohighlevel.com";
|
|
5868
5870
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
5869
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
5871
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path8}`;
|
|
5870
5872
|
const options = { method, headers };
|
|
5871
5873
|
if (body && (method === "POST" || method === "PUT")) {
|
|
5872
5874
|
options.body = JSON.stringify(body);
|
|
@@ -5874,7 +5876,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
5874
5876
|
const response = await fetch(url, options);
|
|
5875
5877
|
if (!response.ok) {
|
|
5876
5878
|
const text2 = await response.text();
|
|
5877
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
5879
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path8}
|
|
5878
5880
|
${text2}`);
|
|
5879
5881
|
}
|
|
5880
5882
|
const text = await response.text();
|
|
@@ -6184,9 +6186,9 @@ function buildUpdateFormPath(formId, locationId2) {
|
|
|
6184
6186
|
function buildUpdateFormBody(name, formData) {
|
|
6185
6187
|
return { name, formData };
|
|
6186
6188
|
}
|
|
6187
|
-
async function formApiRequest(client, method,
|
|
6189
|
+
async function formApiRequest(client, method, path8, body) {
|
|
6188
6190
|
const headers = await client.buildHeaders();
|
|
6189
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
6191
|
+
const url = `https://backend.leadconnectorhq.com/forms${path8}`;
|
|
6190
6192
|
const options = { method, headers };
|
|
6191
6193
|
if (body && (method === "POST" || method === "PUT")) {
|
|
6192
6194
|
options.body = JSON.stringify(body);
|
|
@@ -6194,7 +6196,7 @@ async function formApiRequest(client, method, path7, body) {
|
|
|
6194
6196
|
const response = await fetch(url, options);
|
|
6195
6197
|
if (!response.ok) {
|
|
6196
6198
|
const text2 = await response.text();
|
|
6197
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
6199
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path8}
|
|
6198
6200
|
${text2}`);
|
|
6199
6201
|
}
|
|
6200
6202
|
const text = await response.text();
|
|
@@ -6208,7 +6210,7 @@ ${text2}`);
|
|
|
6208
6210
|
function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
6209
6211
|
const client = builderClient;
|
|
6210
6212
|
if (!client) return;
|
|
6211
|
-
const formRequest = (method,
|
|
6213
|
+
const formRequest = (method, path8, body) => formApiRequest(client, method, path8, body);
|
|
6212
6214
|
server2.tool(
|
|
6213
6215
|
"get_form_full",
|
|
6214
6216
|
"Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns everything the form builder UI shows.",
|
|
@@ -6329,10 +6331,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
6329
6331
|
},
|
|
6330
6332
|
async ({ formId, limit, skip }) => {
|
|
6331
6333
|
try {
|
|
6332
|
-
let
|
|
6333
|
-
if (formId)
|
|
6334
|
-
if (skip)
|
|
6335
|
-
const result = await formRequest("GET",
|
|
6334
|
+
let path8 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
6335
|
+
if (formId) path8 += `&formId=${formId}`;
|
|
6336
|
+
if (skip) path8 += `&skip=${skip}`;
|
|
6337
|
+
const result = await formRequest("GET", path8);
|
|
6336
6338
|
return {
|
|
6337
6339
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
6338
6340
|
};
|
|
@@ -6725,9 +6727,9 @@ var import_zod37 = require("zod");
|
|
|
6725
6727
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
6726
6728
|
const client = builderClient;
|
|
6727
6729
|
if (!client) return;
|
|
6728
|
-
async function pipelineRequest(method,
|
|
6730
|
+
async function pipelineRequest(method, path8, body) {
|
|
6729
6731
|
const headers = await client.buildHeaders();
|
|
6730
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
6732
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path8}`;
|
|
6731
6733
|
const options = { method, headers };
|
|
6732
6734
|
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
6733
6735
|
options.body = JSON.stringify(body);
|
|
@@ -6735,7 +6737,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
|
|
|
6735
6737
|
const response = await fetch(url, options);
|
|
6736
6738
|
if (!response.ok) {
|
|
6737
6739
|
const text2 = await response.text();
|
|
6738
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
6740
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path8}
|
|
6739
6741
|
${text2}`);
|
|
6740
6742
|
}
|
|
6741
6743
|
const text = await response.text();
|
|
@@ -6877,12 +6879,12 @@ ${text2}`);
|
|
|
6877
6879
|
}
|
|
6878
6880
|
|
|
6879
6881
|
// src/tools/location-switcher.ts
|
|
6880
|
-
var
|
|
6882
|
+
var import_zod40 = require("zod");
|
|
6881
6883
|
|
|
6882
6884
|
// src/setup-tool.ts
|
|
6883
6885
|
var os2 = __toESM(require("os"));
|
|
6884
6886
|
var crypto2 = __toESM(require("crypto"));
|
|
6885
|
-
var
|
|
6887
|
+
var import_zod39 = require("zod");
|
|
6886
6888
|
|
|
6887
6889
|
// src/firebase-capture-script.ts
|
|
6888
6890
|
var FIREBASE_CAPTURE_SCRIPT = `(async () => {
|
|
@@ -6969,6 +6971,147 @@ function parseFirebasePaste(input) {
|
|
|
6969
6971
|
};
|
|
6970
6972
|
}
|
|
6971
6973
|
|
|
6974
|
+
// src/interactive-capture.ts
|
|
6975
|
+
var fs4 = __toESM(require("fs"));
|
|
6976
|
+
var path4 = __toESM(require("path"));
|
|
6977
|
+
var import_child_process = require("child_process");
|
|
6978
|
+
var import_zod38 = require("zod");
|
|
6979
|
+
function captureStatePath() {
|
|
6980
|
+
return path4.join(appDataDir(), "interactive-capture.json");
|
|
6981
|
+
}
|
|
6982
|
+
var CaptureStateSchema = import_zod38.z.object({
|
|
6983
|
+
status: import_zod38.z.enum(["waiting_login", "captured", "timeout", "error"]),
|
|
6984
|
+
startedAt: import_zod38.z.string(),
|
|
6985
|
+
updatedAt: import_zod38.z.string(),
|
|
6986
|
+
// Present when status === "captured"
|
|
6987
|
+
ghl_firebase_api_key: import_zod38.z.string().optional(),
|
|
6988
|
+
ghl_user_id: import_zod38.z.string().optional(),
|
|
6989
|
+
ghl_firebase_refresh_token: import_zod38.z.string().optional(),
|
|
6990
|
+
account_email: import_zod38.z.string().optional(),
|
|
6991
|
+
// Present when status === "error"
|
|
6992
|
+
error: import_zod38.z.string().optional()
|
|
6993
|
+
});
|
|
6994
|
+
function readCaptureState() {
|
|
6995
|
+
try {
|
|
6996
|
+
const raw = fs4.readFileSync(captureStatePath(), "utf8");
|
|
6997
|
+
const parsed = CaptureStateSchema.safeParse(JSON.parse(raw));
|
|
6998
|
+
return parsed.success ? parsed.data : null;
|
|
6999
|
+
} catch {
|
|
7000
|
+
return null;
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
function clearCaptureState() {
|
|
7004
|
+
try {
|
|
7005
|
+
fs4.unlinkSync(captureStatePath());
|
|
7006
|
+
} catch {
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
function isStale(state, maxAgeMs) {
|
|
7010
|
+
const t = Date.parse(state.updatedAt);
|
|
7011
|
+
if (Number.isNaN(t)) return true;
|
|
7012
|
+
return Date.now() - t > maxAgeMs;
|
|
7013
|
+
}
|
|
7014
|
+
var EXTRACT_FIREBASE_PAGE_FN = `async () => {
|
|
7015
|
+
const pick = (rows) => {
|
|
7016
|
+
const candidates = rows.filter(r => typeof r?.fbase_key === 'string' && r.fbase_key.startsWith('firebase:authUser:AIza') && r?.value?.apiKey && r?.value?.uid && r?.value?.stsTokenManager?.refreshToken);
|
|
7017
|
+
if (!candidates.length) return null;
|
|
7018
|
+
const v = candidates[0].value;
|
|
7019
|
+
return {
|
|
7020
|
+
ghl_firebase_api_key: v.apiKey,
|
|
7021
|
+
ghl_user_id: v.uid,
|
|
7022
|
+
ghl_firebase_refresh_token: v.stsTokenManager.refreshToken,
|
|
7023
|
+
account_email: v.email || null,
|
|
7024
|
+
};
|
|
7025
|
+
};
|
|
7026
|
+
try {
|
|
7027
|
+
let poisoned = false;
|
|
7028
|
+
if (typeof indexedDB.databases === 'function') {
|
|
7029
|
+
const dbs = await indexedDB.databases();
|
|
7030
|
+
if (dbs.some(d => d.name === 'firebaseLocalStorageDb')) {
|
|
7031
|
+
const db = await new Promise((res, rej) => {
|
|
7032
|
+
const r = indexedDB.open('firebaseLocalStorageDb');
|
|
7033
|
+
r.onsuccess = () => res(r.result);
|
|
7034
|
+
r.onerror = () => rej(r.error);
|
|
7035
|
+
});
|
|
7036
|
+
if (db.objectStoreNames.contains('firebaseLocalStorage')) {
|
|
7037
|
+
const tx = db.transaction('firebaseLocalStorage', 'readonly');
|
|
7038
|
+
const store = tx.objectStore('firebaseLocalStorage');
|
|
7039
|
+
const rows = await new Promise((res, rej) => {
|
|
7040
|
+
const r = store.getAll();
|
|
7041
|
+
r.onsuccess = () => res(r.result);
|
|
7042
|
+
r.onerror = () => rej(r.error);
|
|
7043
|
+
});
|
|
7044
|
+
db.close();
|
|
7045
|
+
const found = pick(rows);
|
|
7046
|
+
if (found) return { found };
|
|
7047
|
+
} else {
|
|
7048
|
+
db.close();
|
|
7049
|
+
poisoned = true;
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
const lsRows = [];
|
|
7054
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
7055
|
+
const k = localStorage.key(i);
|
|
7056
|
+
if (k && k.startsWith('firebase:authUser:AIza')) {
|
|
7057
|
+
try { lsRows.push({ fbase_key: k, value: JSON.parse(localStorage.getItem(k)) }); } catch (e) {}
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
const fromLs = pick(lsRows);
|
|
7061
|
+
if (fromLs) return { found: fromLs };
|
|
7062
|
+
return poisoned ? { poisoned: true } : null;
|
|
7063
|
+
} catch (e) {
|
|
7064
|
+
return null;
|
|
7065
|
+
}
|
|
7066
|
+
}`;
|
|
7067
|
+
var HEAL_POISONED_DB_PAGE_FN = `() => new Promise((resolve) => {
|
|
7068
|
+
const r = indexedDB.deleteDatabase('firebaseLocalStorageDb');
|
|
7069
|
+
const t = setTimeout(() => resolve('timeout'), 5000);
|
|
7070
|
+
r.onsuccess = () => { clearTimeout(t); resolve('deleted'); };
|
|
7071
|
+
r.onblocked = () => { clearTimeout(t); resolve('blocked'); };
|
|
7072
|
+
r.onerror = () => { clearTimeout(t); resolve('error'); };
|
|
7073
|
+
})`;
|
|
7074
|
+
var EXTRACT_FIREBASE_EXPRESSION = `(${EXTRACT_FIREBASE_PAGE_FN})()`;
|
|
7075
|
+
var HEAL_POISONED_DB_EXPRESSION = `(${HEAL_POISONED_DB_PAGE_FN})()`;
|
|
7076
|
+
var HELPER_LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
7077
|
+
var HEADLESS_PROBE_TIMEOUT_MS = 30 * 1e3;
|
|
7078
|
+
var CAPTURED_FRESH_MS = 10 * 60 * 1e3;
|
|
7079
|
+
var WAITING_FRESH_MS = HELPER_LOGIN_TIMEOUT_MS + 30 * 1e3;
|
|
7080
|
+
function helperEntryPath() {
|
|
7081
|
+
return path4.join(__dirname, "capture-helper.js");
|
|
7082
|
+
}
|
|
7083
|
+
function spawnCaptureHelper() {
|
|
7084
|
+
const entry = helperEntryPath();
|
|
7085
|
+
if (!fs4.existsSync(entry)) {
|
|
7086
|
+
return {
|
|
7087
|
+
ok: false,
|
|
7088
|
+
error: `capture helper not found at ${entry} (broken install?)`
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
7091
|
+
try {
|
|
7092
|
+
const child = (0, import_child_process.spawn)(process.execPath, [entry], {
|
|
7093
|
+
detached: true,
|
|
7094
|
+
stdio: "ignore",
|
|
7095
|
+
env: process.env
|
|
7096
|
+
});
|
|
7097
|
+
child.unref();
|
|
7098
|
+
return { ok: true };
|
|
7099
|
+
} catch (e) {
|
|
7100
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
7103
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7104
|
+
async function pollCaptureState(budgetMs, intervalMs = 2e3) {
|
|
7105
|
+
const deadline = Date.now() + budgetMs;
|
|
7106
|
+
let last = null;
|
|
7107
|
+
for (; ; ) {
|
|
7108
|
+
last = readCaptureState();
|
|
7109
|
+
if (last && last.status !== "waiting_login") return last;
|
|
7110
|
+
if (Date.now() >= deadline) return last;
|
|
7111
|
+
await sleep2(Math.min(intervalMs, Math.max(1, deadline - Date.now())));
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
|
|
6972
7115
|
// src/setup-tool.ts
|
|
6973
7116
|
var LICENSE_API = "https://elitedcs.com/api/validate-license";
|
|
6974
7117
|
var CAPTURE_API = "https://elitedcs.com/api/capture-lead";
|
|
@@ -7055,21 +7198,21 @@ async function validateFirebase(firebaseKey, refreshToken) {
|
|
|
7055
7198
|
function registerSetupTool(server2) {
|
|
7056
7199
|
server2.tool(
|
|
7057
7200
|
"setup_ghl_mcp",
|
|
7058
|
-
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all
|
|
7201
|
+
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 229 tools (177 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
|
|
7059
7202
|
{
|
|
7060
|
-
email:
|
|
7061
|
-
license_key:
|
|
7062
|
-
ghl_api_key:
|
|
7063
|
-
ghl_location_id:
|
|
7064
|
-
ghl_company_id:
|
|
7203
|
+
email: import_zod39.z.string().email().describe("Email used at purchase."),
|
|
7204
|
+
license_key: import_zod39.z.string().min(20).describe("License key from your purchase email."),
|
|
7205
|
+
ghl_api_key: import_zod39.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
|
|
7206
|
+
ghl_location_id: import_zod39.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
|
|
7207
|
+
ghl_company_id: import_zod39.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
|
|
7065
7208
|
// v3.25.0: one-paste shortcut. Run `auto_capture_firebase_script` first;
|
|
7066
7209
|
// it returns a console script that fills the clipboard with this exact
|
|
7067
7210
|
// JSON payload. Pasting it here removes the need to fill ghl_user_id,
|
|
7068
7211
|
// ghl_firebase_api_key, and ghl_firebase_refresh_token individually.
|
|
7069
|
-
firebase_paste:
|
|
7070
|
-
ghl_user_id:
|
|
7071
|
-
ghl_firebase_api_key:
|
|
7072
|
-
ghl_firebase_refresh_token:
|
|
7212
|
+
firebase_paste: import_zod39.z.string().optional().describe("(Workflow Builder, one-paste path) Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
|
|
7213
|
+
ghl_user_id: import_zod39.z.string().optional().describe("(Workflow Builder, manual path) Firebase User ID. Prefer firebase_paste instead."),
|
|
7214
|
+
ghl_firebase_api_key: import_zod39.z.string().optional().describe("(Workflow Builder, manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste instead."),
|
|
7215
|
+
ghl_firebase_refresh_token: import_zod39.z.string().optional().describe("(Workflow Builder, manual path) Firebase refresh token. Prefer firebase_paste instead.")
|
|
7073
7216
|
},
|
|
7074
7217
|
async (args) => {
|
|
7075
7218
|
const lic = await validateLicense(args.email, args.license_key);
|
|
@@ -7136,7 +7279,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
7136
7279
|
// verified on every MCP startup. Closes the hand-crafted creds bypass.
|
|
7137
7280
|
signed_attestation: lic.signedAttestation
|
|
7138
7281
|
});
|
|
7139
|
-
const toolCount = workflowBuilderEnabled ? "
|
|
7282
|
+
const toolCount = workflowBuilderEnabled ? "229" : "177";
|
|
7140
7283
|
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
7141
7284
|
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (49 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
7142
7285
|
return {
|
|
@@ -7164,16 +7307,16 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
7164
7307
|
function registerEnableWorkflowBuilderTool(server2) {
|
|
7165
7308
|
server2.tool(
|
|
7166
7309
|
"enable_workflow_builder",
|
|
7167
|
-
"Add Firebase credentials to an existing GHL Command install to unlock 52 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. Requires you've already run setup_ghl_mcp. EASIEST PATH: run `
|
|
7310
|
+
"Add Firebase credentials to an existing GHL Command install to unlock 52 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. 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 177 to 229 after the next Claude restart.",
|
|
7168
7311
|
{
|
|
7169
7312
|
// v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
|
|
7170
7313
|
// get the console script; the script returns a JSON object that pastes
|
|
7171
7314
|
// cleanly into this field. Saves the buyer from picking out three
|
|
7172
7315
|
// separate fields in IndexedDB.
|
|
7173
|
-
firebase_paste:
|
|
7174
|
-
ghl_user_id:
|
|
7175
|
-
ghl_firebase_api_key:
|
|
7176
|
-
ghl_firebase_refresh_token:
|
|
7316
|
+
firebase_paste: import_zod39.z.string().optional().describe("Paste the JSON output from auto_capture_firebase_script here. Replaces the three separate Firebase fields below."),
|
|
7317
|
+
ghl_user_id: import_zod39.z.string().min(10).optional().describe("(Manual path) Firebase User ID (uid). Prefer firebase_paste."),
|
|
7318
|
+
ghl_firebase_api_key: import_zod39.z.string().min(10).optional().describe("(Manual path) Firebase API Key starting with 'AIza'. Prefer firebase_paste."),
|
|
7319
|
+
ghl_firebase_refresh_token: import_zod39.z.string().min(10).optional().describe("(Manual path) Firebase refresh token. Prefer firebase_paste.")
|
|
7177
7320
|
},
|
|
7178
7321
|
async (args) => {
|
|
7179
7322
|
const existing = readCredentials();
|
|
@@ -7245,7 +7388,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
|
7245
7388
|
"",
|
|
7246
7389
|
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen. Without a restart, the workflow builder tools will keep using the OLD Firebase auth from before this call and fail with 401 errors \u2014 even though this tool reported success.",
|
|
7247
7390
|
"",
|
|
7248
|
-
'After restart, all
|
|
7391
|
+
'After restart, all 229 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
7249
7392
|
"",
|
|
7250
7393
|
"Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working in a few weeks (run `health_check` to confirm Firebase auth: FAIL), run `auto_capture_firebase_script` for fresh values and re-run this tool with the new firebase_paste."
|
|
7251
7394
|
].join("\n")
|
|
@@ -7257,7 +7400,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
|
7257
7400
|
function registerFirebaseCaptureScriptTool(server2) {
|
|
7258
7401
|
server2.tool(
|
|
7259
7402
|
"auto_capture_firebase_script",
|
|
7260
|
-
"Get the browser-console script that auto-extracts the 3 Firebase fields needed to enable the Workflow Builder. Run this, copy the script, paste it into Chrome DevTools Console on a tab logged into GHL, press Enter, and the result lands in your clipboard. Then paste the JSON into setup_ghl_mcp's firebase_paste field (or enable_workflow_builder's).
|
|
7403
|
+
"Get the browser-console script that auto-extracts the 3 Firebase fields needed to enable the Workflow Builder. PREFER `capture_firebase_interactive` when Chrome/Edge is installed (one-click, zero pasting); this script is the fallback for locked-down machines and the path for capturing a CLIENT account's Firebase (multi-tenant). Run this, copy the script, paste it into Chrome DevTools Console on a tab logged into GHL, press Enter, and the result lands in your clipboard. Then paste the JSON into setup_ghl_mcp's firebase_paste field (or enable_workflow_builder's).",
|
|
7261
7404
|
{},
|
|
7262
7405
|
async () => {
|
|
7263
7406
|
const steps = [
|
|
@@ -7291,13 +7434,144 @@ function registerFirebaseCaptureScriptTool(server2) {
|
|
|
7291
7434
|
}
|
|
7292
7435
|
);
|
|
7293
7436
|
}
|
|
7437
|
+
function registerInteractiveCaptureTool(server2) {
|
|
7438
|
+
server2.tool(
|
|
7439
|
+
"capture_firebase_interactive",
|
|
7440
|
+
"One-click Workflow Builder unlock (EASIEST PATH \u2014 try this before auto_capture_firebase_script). Opens a Chrome window where you just log into GHL; the MCP captures the Firebase credentials itself and saves them. No DevTools, no pasting. Re-run the same tool if the first call says it's still waiting for your login. After the first successful run, future re-captures (refresh-token rotations) happen silently with no window at all. Falls back cleanly: if no Chrome/Edge is installed, use auto_capture_firebase_script instead.",
|
|
7441
|
+
{},
|
|
7442
|
+
async () => {
|
|
7443
|
+
const existing = readCredentials();
|
|
7444
|
+
if (!existing) {
|
|
7445
|
+
return {
|
|
7446
|
+
content: [{
|
|
7447
|
+
type: "text",
|
|
7448
|
+
text: "No existing credentials found at " + credentialsPath() + ".\n\nRun setup_ghl_mcp first to register your license and basic GHL credentials, then run capture_firebase_interactive to unlock the Workflow Builder."
|
|
7449
|
+
}],
|
|
7450
|
+
isError: true
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
const persistCaptured = async (state) => {
|
|
7454
|
+
const fbApi = state.ghl_firebase_api_key;
|
|
7455
|
+
const fbRefresh = state.ghl_firebase_refresh_token;
|
|
7456
|
+
const userId = state.ghl_user_id;
|
|
7457
|
+
const fb = await validateFirebase(fbApi, fbRefresh);
|
|
7458
|
+
if (!fb.ok) {
|
|
7459
|
+
clearCaptureState();
|
|
7460
|
+
return {
|
|
7461
|
+
content: [{
|
|
7462
|
+
type: "text",
|
|
7463
|
+
text: `Captured credentials were rejected by Firebase: ${fb.error}
|
|
7464
|
+
|
|
7465
|
+
The login in the capture window may belong to the wrong GHL account, or the session is stale. Run capture_firebase_interactive again and log into the SAME account you use with GHL Command.`
|
|
7466
|
+
}],
|
|
7467
|
+
isError: true
|
|
7468
|
+
};
|
|
7469
|
+
}
|
|
7470
|
+
writeCredentials({
|
|
7471
|
+
...existing,
|
|
7472
|
+
ghl_user_id: userId,
|
|
7473
|
+
ghl_firebase_api_key: fbApi,
|
|
7474
|
+
ghl_firebase_refresh_token: fbRefresh
|
|
7475
|
+
});
|
|
7476
|
+
clearCaptureState();
|
|
7477
|
+
return {
|
|
7478
|
+
content: [{
|
|
7479
|
+
type: "text",
|
|
7480
|
+
text: [
|
|
7481
|
+
"Workflow Builder enabled!",
|
|
7482
|
+
"",
|
|
7483
|
+
`Firebase credentials captured${state.account_email ? ` from ${state.account_email}` : ""}, verified, and saved to credentials.json.`,
|
|
7484
|
+
"",
|
|
7485
|
+
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen.",
|
|
7486
|
+
"",
|
|
7487
|
+
'After restart, all 229 tools load. Try: "List my workflows in full detail".',
|
|
7488
|
+
"",
|
|
7489
|
+
"Future token rotations re-capture silently \u2014 if workflow tools ever 401, just run capture_firebase_interactive again; no window should appear."
|
|
7490
|
+
].join("\n")
|
|
7491
|
+
}]
|
|
7492
|
+
};
|
|
7493
|
+
};
|
|
7494
|
+
const fallbackNote = "Fallback that always works: run `auto_capture_firebase_script` and paste the script into Chrome's Console on a logged-in GHL tab (type `allow pasting` first if Chrome blocks it).";
|
|
7495
|
+
const prior = readCaptureState();
|
|
7496
|
+
if (prior?.status === "captured" && !isStale(prior, CAPTURED_FRESH_MS)) {
|
|
7497
|
+
return persistCaptured(prior);
|
|
7498
|
+
}
|
|
7499
|
+
if (prior?.status === "waiting_login" && !isStale(prior, WAITING_FRESH_MS)) {
|
|
7500
|
+
const polled2 = await pollCaptureState(6e4);
|
|
7501
|
+
if (polled2?.status === "captured") return persistCaptured(polled2);
|
|
7502
|
+
if (polled2?.status === "error") {
|
|
7503
|
+
clearCaptureState();
|
|
7504
|
+
return {
|
|
7505
|
+
content: [{ type: "text", text: `Capture failed: ${polled2.error}
|
|
7506
|
+
|
|
7507
|
+
${fallbackNote}` }],
|
|
7508
|
+
isError: true
|
|
7509
|
+
};
|
|
7510
|
+
}
|
|
7511
|
+
return {
|
|
7512
|
+
content: [{
|
|
7513
|
+
type: "text",
|
|
7514
|
+
text: "The capture window is still open and waiting for your GHL login. Finish logging in (2FA is fine), leave the window alone for a few seconds, then run capture_firebase_interactive again to save the result."
|
|
7515
|
+
}]
|
|
7516
|
+
};
|
|
7517
|
+
}
|
|
7518
|
+
clearCaptureState();
|
|
7519
|
+
const spawned = spawnCaptureHelper();
|
|
7520
|
+
if (!spawned.ok) {
|
|
7521
|
+
return {
|
|
7522
|
+
content: [{ type: "text", text: `Couldn't start the capture helper: ${spawned.error}
|
|
7523
|
+
|
|
7524
|
+
${fallbackNote}` }],
|
|
7525
|
+
isError: true
|
|
7526
|
+
};
|
|
7527
|
+
}
|
|
7528
|
+
const polled = await pollCaptureState(75e3);
|
|
7529
|
+
if (polled?.status === "captured") return persistCaptured(polled);
|
|
7530
|
+
if (polled?.status === "error") {
|
|
7531
|
+
clearCaptureState();
|
|
7532
|
+
return {
|
|
7533
|
+
content: [{ type: "text", text: `Capture failed: ${polled.error}
|
|
7534
|
+
|
|
7535
|
+
${fallbackNote}` }],
|
|
7536
|
+
isError: true
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
if (polled?.status === "timeout") {
|
|
7540
|
+
clearCaptureState();
|
|
7541
|
+
return {
|
|
7542
|
+
content: [{
|
|
7543
|
+
type: "text",
|
|
7544
|
+
text: `The capture window timed out waiting for a login. Run capture_firebase_interactive again when you're ready to log in.
|
|
7545
|
+
|
|
7546
|
+
${fallbackNote}`
|
|
7547
|
+
}],
|
|
7548
|
+
isError: true
|
|
7549
|
+
};
|
|
7550
|
+
}
|
|
7551
|
+
return {
|
|
7552
|
+
content: [{
|
|
7553
|
+
type: "text",
|
|
7554
|
+
text: [
|
|
7555
|
+
`A Chrome window just opened on the GoHighLevel login page (it may say "Chrome is being controlled by automated test software" \u2014 that's this tool).`,
|
|
7556
|
+
"",
|
|
7557
|
+
"**Log into your GHL account in that window** (2FA is fine). That's your entire job \u2014 the tool reads the credentials itself.",
|
|
7558
|
+
"",
|
|
7559
|
+
"Once you're logged in, run `capture_firebase_interactive` again and it will save the result. You only ever do this login once: future re-captures happen silently.",
|
|
7560
|
+
"",
|
|
7561
|
+
"Don't see a window? " + fallbackNote
|
|
7562
|
+
].join("\n")
|
|
7563
|
+
}]
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
);
|
|
7567
|
+
}
|
|
7294
7568
|
function registerLeadCaptureTool(server2) {
|
|
7295
7569
|
server2.tool(
|
|
7296
7570
|
"request_license",
|
|
7297
7571
|
"Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97/mo \u2014 every sub-account you manage, 3-machine activation, includes the only programmatic GHL workflow builder. Leave your email and we'll send the purchase link + setup help; the tool also returns where to buy right now.",
|
|
7298
7572
|
{
|
|
7299
|
-
email:
|
|
7300
|
-
name:
|
|
7573
|
+
email: import_zod39.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
|
|
7574
|
+
name: import_zod39.z.string().optional().describe("Your name (optional).")
|
|
7301
7575
|
},
|
|
7302
7576
|
async (args) => {
|
|
7303
7577
|
const buyUrl = "https://ghlcommand.com";
|
|
@@ -7416,7 +7690,7 @@ Token registry: ${registeredCount} location(s) registered${versionLine}`
|
|
|
7416
7690
|
"switch_location",
|
|
7417
7691
|
"Switch the active GHL sub-account. Automatically swaps the API key from the token registry if available. After switching, all tools default to the new location.",
|
|
7418
7692
|
{
|
|
7419
|
-
locationId:
|
|
7693
|
+
locationId: import_zod40.z.string().describe("The Location ID to switch to.")
|
|
7420
7694
|
},
|
|
7421
7695
|
async ({ locationId: locationId2 }) => withSwitchLock(async () => {
|
|
7422
7696
|
const previousId = client.defaultLocationId;
|
|
@@ -7489,9 +7763,9 @@ Still on: ${previousId || "none"}${hint}` }],
|
|
|
7489
7763
|
"register_location",
|
|
7490
7764
|
"Add a GHL sub-account to the token registry so switch_location can automatically use its API key. Each sub-account needs its own Private Integration key created in GHL Settings > Integrations.",
|
|
7491
7765
|
{
|
|
7492
|
-
locationId:
|
|
7493
|
-
name:
|
|
7494
|
-
apiKey:
|
|
7766
|
+
locationId: import_zod40.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
|
|
7767
|
+
name: import_zod40.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
|
|
7768
|
+
apiKey: import_zod40.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
|
|
7495
7769
|
},
|
|
7496
7770
|
async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
|
|
7497
7771
|
if (!registry2) {
|
|
@@ -7548,7 +7822,7 @@ The API key could not access location ${locationId2}. Make sure:
|
|
|
7548
7822
|
"register_agency_key",
|
|
7549
7823
|
"Store the AGENCY-level (company-scoped) API key in the token registry. This key powers agency-wide tools: list_snapshots, create_snapshot_share_link, and list_available_locations across all sub-accounts. Create it at the AGENCY level in GHL (Agency Settings > Private Integrations) \u2014 it is different from a sub-account's key. The key is validated before saving.",
|
|
7550
7824
|
{
|
|
7551
|
-
apiKey:
|
|
7825
|
+
apiKey: import_zod40.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
|
|
7552
7826
|
},
|
|
7553
7827
|
async ({ apiKey: apiKey2 }) => {
|
|
7554
7828
|
if (!registry2) {
|
|
@@ -7602,7 +7876,7 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
|
|
|
7602
7876
|
"unregister_location",
|
|
7603
7877
|
"Remove a GHL sub-account from the token registry.",
|
|
7604
7878
|
{
|
|
7605
|
-
locationId:
|
|
7879
|
+
locationId: import_zod40.z.string().describe("The Location ID to remove.")
|
|
7606
7880
|
},
|
|
7607
7881
|
async ({ locationId: locationId2 }) => {
|
|
7608
7882
|
if (!registry2) {
|
|
@@ -7628,12 +7902,12 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
|
|
|
7628
7902
|
"register_company_firebase",
|
|
7629
7903
|
"Register a GHL company's Firebase credentials so the workflow builder and all Firebase-gated tools work when you switch into THAT company's sub-accounts. Firebase refresh tokens are company-scoped, so managing a client's GHL (e.g. an account where you're an admin user) requires that company's own token. Capture the values from a browser session logged into the client's account. The tool stores them under the company ID the Firebase token itself authenticates as (decoded from the token), so you do NOT need to hunt down the exact internal company ID \u2014 pass whatever ID you have and it self-corrects. After this, switch_location to any of that company's locations authenticates the workflow builder correctly. DevTools capture steps: elitedcs.com/ghl-mcp-firebase.",
|
|
7630
7904
|
{
|
|
7631
|
-
companyId:
|
|
7632
|
-
name:
|
|
7633
|
-
ghl_firebase_refresh_token:
|
|
7634
|
-
ghl_user_id:
|
|
7635
|
-
ghl_firebase_api_key:
|
|
7636
|
-
test_location_id:
|
|
7905
|
+
companyId: import_zod40.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
|
|
7906
|
+
name: import_zod40.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
|
|
7907
|
+
ghl_firebase_refresh_token: import_zod40.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
|
|
7908
|
+
ghl_user_id: import_zod40.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
|
|
7909
|
+
ghl_firebase_api_key: import_zod40.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
|
|
7910
|
+
test_location_id: import_zod40.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
|
|
7637
7911
|
},
|
|
7638
7912
|
async (args) => {
|
|
7639
7913
|
if (!registry2) {
|
|
@@ -7737,7 +8011,7 @@ Now run switch_location to any of this company's sub-accounts \u2014 the workflo
|
|
|
7737
8011
|
"unregister_company_firebase",
|
|
7738
8012
|
"Remove a company's Firebase credentials from the registry. Workflow-builder tools will stop working for that company's sub-accounts until re-registered.",
|
|
7739
8013
|
{
|
|
7740
|
-
companyId:
|
|
8014
|
+
companyId: import_zod40.z.string().describe("The company ID to remove Firebase credentials for.")
|
|
7741
8015
|
},
|
|
7742
8016
|
async ({ companyId }) => {
|
|
7743
8017
|
if (!registry2) {
|
|
@@ -7805,8 +8079,8 @@ ${lines.join("\n")}
|
|
|
7805
8079
|
"list_available_locations",
|
|
7806
8080
|
"List all GHL sub-accounts (locations) accessible with the current or agency API key. Shows locations that exist in the GHL account \u2014 use register_location to add their tokens. Offset-based pagination via skip/limit.",
|
|
7807
8081
|
{
|
|
7808
|
-
limit:
|
|
7809
|
-
skip:
|
|
8082
|
+
limit: import_zod40.z.number().optional().describe("Max locations to return. Defaults to 20."),
|
|
8083
|
+
skip: import_zod40.z.number().optional().describe("Number to skip for pagination.")
|
|
7810
8084
|
},
|
|
7811
8085
|
async ({ limit, skip }) => {
|
|
7812
8086
|
try {
|
|
@@ -7849,7 +8123,7 @@ ${lines.join("\n")}
|
|
|
7849
8123
|
}
|
|
7850
8124
|
|
|
7851
8125
|
// src/tools/bulk-operations.ts
|
|
7852
|
-
var
|
|
8126
|
+
var import_zod41 = require("zod");
|
|
7853
8127
|
function delay(ms) {
|
|
7854
8128
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
7855
8129
|
}
|
|
@@ -7861,8 +8135,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7861
8135
|
"bulk_add_tags",
|
|
7862
8136
|
"Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
|
|
7863
8137
|
{
|
|
7864
|
-
contactIds:
|
|
7865
|
-
tags:
|
|
8138
|
+
contactIds: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
|
|
8139
|
+
tags: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
|
|
7866
8140
|
},
|
|
7867
8141
|
async ({ contactIds, tags }) => {
|
|
7868
8142
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -7884,8 +8158,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7884
8158
|
"bulk_remove_tags",
|
|
7885
8159
|
"Remove tags from multiple contacts at once. Rate-limited.",
|
|
7886
8160
|
{
|
|
7887
|
-
contactIds:
|
|
7888
|
-
tags:
|
|
8161
|
+
contactIds: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
|
|
8162
|
+
tags: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
|
|
7889
8163
|
},
|
|
7890
8164
|
async ({ contactIds, tags }) => {
|
|
7891
8165
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -7906,8 +8180,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7906
8180
|
"bulk_update_contacts",
|
|
7907
8181
|
"Update the same field(s) on multiple contacts at once. Rate-limited. Example: set a custom field value, change source, update address for a batch of contacts.",
|
|
7908
8182
|
{
|
|
7909
|
-
contactIds:
|
|
7910
|
-
fields:
|
|
8183
|
+
contactIds: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
|
|
8184
|
+
fields: import_zod41.z.record(import_zod41.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
|
|
7911
8185
|
},
|
|
7912
8186
|
async ({ contactIds, fields }) => {
|
|
7913
8187
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -7928,8 +8202,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7928
8202
|
"bulk_add_to_workflow",
|
|
7929
8203
|
"Enroll multiple contacts into a workflow at once. Rate-limited.",
|
|
7930
8204
|
{
|
|
7931
|
-
contactIds:
|
|
7932
|
-
workflowId:
|
|
8205
|
+
contactIds: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
|
|
8206
|
+
workflowId: import_zod41.z.string().describe("The workflow ID to enroll contacts into.")
|
|
7933
8207
|
},
|
|
7934
8208
|
async ({ contactIds, workflowId }) => {
|
|
7935
8209
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -7950,8 +8224,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7950
8224
|
"bulk_delete_contacts",
|
|
7951
8225
|
"Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
|
|
7952
8226
|
{
|
|
7953
|
-
contactIds:
|
|
7954
|
-
confirm:
|
|
8227
|
+
contactIds: import_zod41.z.array(import_zod41.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
|
|
8228
|
+
confirm: import_zod41.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
|
|
7955
8229
|
},
|
|
7956
8230
|
async ({ contactIds, confirm }) => {
|
|
7957
8231
|
if (confirm !== "DELETE") {
|
|
@@ -7974,7 +8248,7 @@ function registerBulkOperationTools(server2, client) {
|
|
|
7974
8248
|
}
|
|
7975
8249
|
|
|
7976
8250
|
// src/tools/account-export.ts
|
|
7977
|
-
var
|
|
8251
|
+
var import_zod42 = require("zod");
|
|
7978
8252
|
function delay2(ms) {
|
|
7979
8253
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
7980
8254
|
}
|
|
@@ -7984,8 +8258,8 @@ function registerAccountExportTools(server2, client) {
|
|
|
7984
8258
|
"export_account",
|
|
7985
8259
|
"Export a complete inventory of the GHL sub-account: location info, contacts (count + sample), pipelines with stages, workflows (with full actions if builder auth is configured), funnels with pages, forms, custom fields, custom values, tags, calendars, and users. Returns a comprehensive JSON report for auditing or backup.",
|
|
7986
8260
|
{
|
|
7987
|
-
locationId:
|
|
7988
|
-
includeContacts:
|
|
8261
|
+
locationId: import_zod42.z.string().optional().describe("Location ID to export. Uses default if not specified."),
|
|
8262
|
+
includeContacts: import_zod42.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
|
|
7989
8263
|
},
|
|
7990
8264
|
async ({ locationId: locationId2, includeContacts }) => {
|
|
7991
8265
|
try {
|
|
@@ -8113,8 +8387,8 @@ function registerAccountExportTools(server2, client) {
|
|
|
8113
8387
|
"compare_locations",
|
|
8114
8388
|
"Compare two GHL sub-accounts side by side \u2014 shows differences in pipelines, workflows, custom fields, tags, forms, and funnels. Useful for ensuring consistency across locations or auditing before/after changes.",
|
|
8115
8389
|
{
|
|
8116
|
-
locationA:
|
|
8117
|
-
locationB:
|
|
8390
|
+
locationA: import_zod42.z.string().describe("First Location ID."),
|
|
8391
|
+
locationB: import_zod42.z.string().describe("Second Location ID.")
|
|
8118
8392
|
},
|
|
8119
8393
|
async ({ locationA, locationB }) => {
|
|
8120
8394
|
try {
|
|
@@ -8192,7 +8466,7 @@ function registerAccountExportTools(server2, client) {
|
|
|
8192
8466
|
}
|
|
8193
8467
|
|
|
8194
8468
|
// src/tools/workflow-cloner.ts
|
|
8195
|
-
var
|
|
8469
|
+
var import_zod43 = require("zod");
|
|
8196
8470
|
var crypto3 = __toESM(require("crypto"));
|
|
8197
8471
|
function registerWorkflowClonerTools(server2, builderClient) {
|
|
8198
8472
|
const client = builderClient;
|
|
@@ -8201,8 +8475,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
8201
8475
|
"clone_workflow",
|
|
8202
8476
|
"Deep clone a workflow \u2014 creates an exact copy with new IDs for all actions, triggers, and references. The clone starts as a draft. Useful for creating templates or duplicating workflows across projects.",
|
|
8203
8477
|
{
|
|
8204
|
-
sourceWorkflowId:
|
|
8205
|
-
newName:
|
|
8478
|
+
sourceWorkflowId: import_zod43.z.string().describe("The workflow ID to clone."),
|
|
8479
|
+
newName: import_zod43.z.string().describe("Name for the cloned workflow.")
|
|
8206
8480
|
},
|
|
8207
8481
|
async ({ sourceWorkflowId, newName }) => {
|
|
8208
8482
|
try {
|
|
@@ -8291,15 +8565,15 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
8291
8565
|
}
|
|
8292
8566
|
|
|
8293
8567
|
// src/tools/smart-lists.ts
|
|
8294
|
-
var
|
|
8568
|
+
var import_zod44 = require("zod");
|
|
8295
8569
|
var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
|
|
8296
8570
|
var OBJECT_KEYS = ["contacts", "opportunity"];
|
|
8297
8571
|
function registerSmartListTools(server2, builderClient) {
|
|
8298
8572
|
const client = builderClient;
|
|
8299
8573
|
if (!client) return;
|
|
8300
|
-
async function smartListRequest(method,
|
|
8574
|
+
async function smartListRequest(method, path8, body) {
|
|
8301
8575
|
const headers = await client.buildHeaders();
|
|
8302
|
-
const url = `${SMARTLIST_BASE}${
|
|
8576
|
+
const url = `${SMARTLIST_BASE}${path8}`;
|
|
8303
8577
|
const options = { method, headers };
|
|
8304
8578
|
if (body && (method === "POST" || method === "PUT")) {
|
|
8305
8579
|
options.body = JSON.stringify(body);
|
|
@@ -8307,7 +8581,7 @@ function registerSmartListTools(server2, builderClient) {
|
|
|
8307
8581
|
const response = await fetch(url, options);
|
|
8308
8582
|
if (!response.ok) {
|
|
8309
8583
|
const text2 = await response.text();
|
|
8310
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
8584
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path8}
|
|
8311
8585
|
${text2}`);
|
|
8312
8586
|
}
|
|
8313
8587
|
const text = await response.text();
|
|
@@ -8318,11 +8592,11 @@ ${text2}`);
|
|
|
8318
8592
|
"list_smart_lists",
|
|
8319
8593
|
"List smart lists (dynamic / saved-filter lists) in a location. Smart Lists are saved searches over contacts or opportunities \u2014 agencies use them to segment by complex criteria. Filters and columns aren't returned in the list view; use get_smart_list for the full filter spec.",
|
|
8320
8594
|
{
|
|
8321
|
-
objectKey:
|
|
8322
|
-
query:
|
|
8323
|
-
limit:
|
|
8324
|
-
startAfter:
|
|
8325
|
-
locationId:
|
|
8595
|
+
objectKey: import_zod44.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
|
|
8596
|
+
query: import_zod44.z.string().optional().describe("Free-text search across smart list names."),
|
|
8597
|
+
limit: import_zod44.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
|
|
8598
|
+
startAfter: import_zod44.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
|
|
8599
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8326
8600
|
},
|
|
8327
8601
|
async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
|
|
8328
8602
|
try {
|
|
@@ -8342,8 +8616,8 @@ ${text2}`);
|
|
|
8342
8616
|
"get_smart_list",
|
|
8343
8617
|
"Get a single smart list by ID with its full configuration: filters, columns, permissions, and metadata. The filters array is what defines who/what is in the list.",
|
|
8344
8618
|
{
|
|
8345
|
-
listId:
|
|
8346
|
-
locationId:
|
|
8619
|
+
listId: import_zod44.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
|
|
8620
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8347
8621
|
},
|
|
8348
8622
|
async ({ listId, locationId: locationId2 }) => {
|
|
8349
8623
|
try {
|
|
@@ -8359,13 +8633,13 @@ ${text2}`);
|
|
|
8359
8633
|
"create_smart_list",
|
|
8360
8634
|
"Create a new smart list (dynamic filter list). Required: name + objectKey. Filters define the saved-search criteria \u2014 pass an empty array to create an empty list and add filters later via update_smart_list. The shape of filters/columns is opaque here; query an existing smart list with get_smart_list to see the format GHL expects.",
|
|
8361
8635
|
{
|
|
8362
|
-
name:
|
|
8363
|
-
objectKey:
|
|
8364
|
-
filters:
|
|
8365
|
-
columns:
|
|
8366
|
-
pipelineIds:
|
|
8367
|
-
defaultInPipelines:
|
|
8368
|
-
locationId:
|
|
8636
|
+
name: import_zod44.z.string().describe("Display name for the smart list."),
|
|
8637
|
+
objectKey: import_zod44.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
|
|
8638
|
+
filters: import_zod44.z.array(import_zod44.z.record(import_zod44.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
|
|
8639
|
+
columns: import_zod44.z.array(import_zod44.z.record(import_zod44.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
|
|
8640
|
+
pipelineIds: import_zod44.z.array(import_zod44.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
|
|
8641
|
+
defaultInPipelines: import_zod44.z.array(import_zod44.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
|
|
8642
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8369
8643
|
},
|
|
8370
8644
|
async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
8371
8645
|
try {
|
|
@@ -8386,13 +8660,13 @@ ${text2}`);
|
|
|
8386
8660
|
"update_smart_list",
|
|
8387
8661
|
"Update an existing smart list's name, filters, or columns. The objectKey CANNOT be changed after creation (GHL rejects with 422 if you try). Use get_smart_list first to inspect the current filters; partial updates work \u2014 pass only the fields you want to change.",
|
|
8388
8662
|
{
|
|
8389
|
-
listId:
|
|
8390
|
-
name:
|
|
8391
|
-
filters:
|
|
8392
|
-
columns:
|
|
8393
|
-
pipelineIds:
|
|
8394
|
-
defaultInPipelines:
|
|
8395
|
-
locationId:
|
|
8663
|
+
listId: import_zod44.z.string().describe("The smart list ID to update."),
|
|
8664
|
+
name: import_zod44.z.string().optional().describe("New display name."),
|
|
8665
|
+
filters: import_zod44.z.array(import_zod44.z.record(import_zod44.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
|
|
8666
|
+
columns: import_zod44.z.array(import_zod44.z.record(import_zod44.z.unknown())).optional().describe("Replace the column array entirely."),
|
|
8667
|
+
pipelineIds: import_zod44.z.array(import_zod44.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
|
|
8668
|
+
defaultInPipelines: import_zod44.z.array(import_zod44.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
|
|
8669
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8396
8670
|
},
|
|
8397
8671
|
async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
8398
8672
|
try {
|
|
@@ -8417,9 +8691,9 @@ ${text2}`);
|
|
|
8417
8691
|
"delete_smart_list",
|
|
8418
8692
|
"Permanently delete a smart list. IRREVERSIBLE. The list configuration is removed but the contacts/opportunities themselves are NOT touched \u2014 smart lists are just saved filters. Any workflow trigger / dashboard / report that referenced this list by ID will stop working.",
|
|
8419
8693
|
{
|
|
8420
|
-
listId:
|
|
8421
|
-
confirm:
|
|
8422
|
-
locationId:
|
|
8694
|
+
listId: import_zod44.z.string().describe("The smart list ID to delete."),
|
|
8695
|
+
confirm: import_zod44.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
|
|
8696
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8423
8697
|
},
|
|
8424
8698
|
async ({ listId, locationId: locationId2 }) => {
|
|
8425
8699
|
try {
|
|
@@ -8434,17 +8708,17 @@ ${text2}`);
|
|
|
8434
8708
|
}
|
|
8435
8709
|
|
|
8436
8710
|
// src/tools/reputation.ts
|
|
8437
|
-
var
|
|
8711
|
+
var import_zod45 = require("zod");
|
|
8438
8712
|
var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
|
|
8439
8713
|
function registerReputationTools(server2, builderClient) {
|
|
8440
8714
|
const client = builderClient;
|
|
8441
8715
|
if (!client) return;
|
|
8442
|
-
async function reputationRequest(method,
|
|
8716
|
+
async function reputationRequest(method, path8) {
|
|
8443
8717
|
const headers = await client.buildHeaders();
|
|
8444
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
8718
|
+
const response = await fetch(`${REPUTATION_BASE}${path8}`, { method, headers });
|
|
8445
8719
|
if (!response.ok) {
|
|
8446
8720
|
const text2 = await response.text();
|
|
8447
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
8721
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path8}
|
|
8448
8722
|
${text2}`);
|
|
8449
8723
|
}
|
|
8450
8724
|
const text = await response.text();
|
|
@@ -8456,7 +8730,7 @@ ${text2}`);
|
|
|
8456
8730
|
"get_review_link_list",
|
|
8457
8731
|
"List the review-link destinations configured for a location \u2014 the platforms (Google, Facebook, etc.) where review requests send contacts. Each entry has a label and the public review URL. Useful for: building review-request workflows (the workflow goal condition `review_request_clicked` references these review-link ids), and auditing which review platforms a sub-account has connected.",
|
|
8458
8732
|
{
|
|
8459
|
-
locationId:
|
|
8733
|
+
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8460
8734
|
},
|
|
8461
8735
|
async ({ locationId: locationId2 }) => {
|
|
8462
8736
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8468,11 +8742,11 @@ ${text2}`);
|
|
|
8468
8742
|
"list_reviews",
|
|
8469
8743
|
"List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, and source. Supports paging and an optional rating filter. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
|
|
8470
8744
|
{
|
|
8471
|
-
locationId:
|
|
8472
|
-
pageNumber:
|
|
8473
|
-
pageSize:
|
|
8474
|
-
rating:
|
|
8475
|
-
includeDeleted:
|
|
8745
|
+
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
|
|
8746
|
+
pageNumber: import_zod45.z.number().optional().describe("1-based page number. Defaults to 1."),
|
|
8747
|
+
pageSize: import_zod45.z.number().optional().describe("Results per page. Defaults to 10."),
|
|
8748
|
+
rating: import_zod45.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
|
|
8749
|
+
includeDeleted: import_zod45.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
|
|
8476
8750
|
},
|
|
8477
8751
|
async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
|
|
8478
8752
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8502,7 +8776,7 @@ function buildReviewsQuery(locationId2, opts = {}) {
|
|
|
8502
8776
|
}
|
|
8503
8777
|
|
|
8504
8778
|
// src/tools/email-campaigns.ts
|
|
8505
|
-
var
|
|
8779
|
+
var import_zod46 = require("zod");
|
|
8506
8780
|
var SVC_BASE = "https://services.leadconnectorhq.com";
|
|
8507
8781
|
function registerEmailCampaignTools(server2, builderClient) {
|
|
8508
8782
|
const client = builderClient;
|
|
@@ -8512,15 +8786,15 @@ function registerEmailCampaignTools(server2, builderClient) {
|
|
|
8512
8786
|
"create_email_campaign",
|
|
8513
8787
|
"Create an email campaign / broadcast DRAFT from an existing email template. Requires a templateId (create one first with create_email_template). The campaign is created as a draft \u2014 to actually SEND or schedule it, finish in the GHL UI: the send/schedule endpoint isn't available through the API yet. There's also no API delete for campaigns, so drafts created here are removed via the GHL UI. Despite those limits, this gets the campaign 90% built \u2014 template, subject, sender, name all set programmatically.",
|
|
8514
8788
|
{
|
|
8515
|
-
templateId:
|
|
8516
|
-
name:
|
|
8517
|
-
subject:
|
|
8518
|
-
fromName:
|
|
8519
|
-
fromEmail:
|
|
8520
|
-
isPlainText:
|
|
8521
|
-
enableResendToUnopened:
|
|
8522
|
-
hasUtmTracking:
|
|
8523
|
-
locationId:
|
|
8789
|
+
templateId: import_zod46.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
|
|
8790
|
+
name: import_zod46.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
|
|
8791
|
+
subject: import_zod46.z.string().optional().describe("Email subject line recipients see."),
|
|
8792
|
+
fromName: import_zod46.z.string().optional().describe("Sender display name."),
|
|
8793
|
+
fromEmail: import_zod46.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
|
|
8794
|
+
isPlainText: import_zod46.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
|
|
8795
|
+
enableResendToUnopened: import_zod46.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
|
|
8796
|
+
hasUtmTracking: import_zod46.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
|
|
8797
|
+
locationId: import_zod46.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8524
8798
|
},
|
|
8525
8799
|
async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
|
|
8526
8800
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8554,21 +8828,21 @@ ${text2}`);
|
|
|
8554
8828
|
}
|
|
8555
8829
|
|
|
8556
8830
|
// src/tools/memberships.ts
|
|
8557
|
-
var
|
|
8831
|
+
var import_zod47 = require("zod");
|
|
8558
8832
|
var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
|
|
8559
8833
|
function registerMembershipTools(server2, builderClient) {
|
|
8560
8834
|
const client = builderClient;
|
|
8561
8835
|
if (!client) return;
|
|
8562
|
-
async function membershipRequest(
|
|
8836
|
+
async function membershipRequest(path8, method = "GET", body) {
|
|
8563
8837
|
const headers = await client.buildHeaders();
|
|
8564
|
-
const response = await fetch(`${MEMBERSHIP_BASE}${
|
|
8838
|
+
const response = await fetch(`${MEMBERSHIP_BASE}${path8}`, {
|
|
8565
8839
|
method,
|
|
8566
8840
|
headers,
|
|
8567
8841
|
body: body ? JSON.stringify(body) : void 0
|
|
8568
8842
|
});
|
|
8569
8843
|
if (!response.ok) {
|
|
8570
8844
|
const text2 = await response.text();
|
|
8571
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
8845
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path8}
|
|
8572
8846
|
${text2}`);
|
|
8573
8847
|
}
|
|
8574
8848
|
const text = await response.text();
|
|
@@ -8580,7 +8854,7 @@ ${text2}`);
|
|
|
8580
8854
|
"list_membership_offers",
|
|
8581
8855
|
"List a location's membership offers and products in one call. Returns { products: [...], offers: [...] }. Products are courses/communities; offers are the access grants (what a contact gets enrolled in). Use the returned ids with membership trigger conditions like offer_access_granted / product_completed.",
|
|
8582
8856
|
{
|
|
8583
|
-
locationId:
|
|
8857
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8584
8858
|
},
|
|
8585
8859
|
async ({ locationId: locationId2 }) => {
|
|
8586
8860
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8592,8 +8866,8 @@ ${text2}`);
|
|
|
8592
8866
|
"list_membership_categories",
|
|
8593
8867
|
"List all membership/course categories in a location. Categories group lessons inside a course/product. Use the returned ids with the category_completed / category_started trigger conditions. READ-ONLY.",
|
|
8594
8868
|
{
|
|
8595
|
-
limit:
|
|
8596
|
-
locationId:
|
|
8869
|
+
limit: import_zod47.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
|
|
8870
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8597
8871
|
},
|
|
8598
8872
|
async ({ limit, locationId: locationId2 }) => {
|
|
8599
8873
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8605,8 +8879,8 @@ ${text2}`);
|
|
|
8605
8879
|
"list_membership_lessons",
|
|
8606
8880
|
"List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
|
|
8607
8881
|
{
|
|
8608
|
-
limit:
|
|
8609
|
-
locationId:
|
|
8882
|
+
limit: import_zod47.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
|
|
8883
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8610
8884
|
},
|
|
8611
8885
|
async ({ limit, locationId: locationId2 }) => {
|
|
8612
8886
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8618,9 +8892,9 @@ ${text2}`);
|
|
|
8618
8892
|
"create_course",
|
|
8619
8893
|
"Create a membership course (a 'product') in a location. Creates the course shell with a title and description; add categories (create_membership_category) and lessons (create_membership_lesson) into it, and an offer (create_membership_offer) to grant access. Returns the new product, including its id. Requires Firebase auth.",
|
|
8620
8894
|
{
|
|
8621
|
-
title:
|
|
8622
|
-
description:
|
|
8623
|
-
locationId:
|
|
8895
|
+
title: import_zod47.z.string().describe("Course title."),
|
|
8896
|
+
description: import_zod47.z.string().optional().describe("Course description. Defaults to empty."),
|
|
8897
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8624
8898
|
},
|
|
8625
8899
|
async ({ title, description, locationId: locationId2 }) => {
|
|
8626
8900
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8632,13 +8906,13 @@ ${text2}`);
|
|
|
8632
8906
|
"create_membership_category",
|
|
8633
8907
|
"Create a category (module/section) inside a membership course. Categories group lessons. Needs the productId of the course (from create_course or list_membership_offers). Returns the new category, including its id (use it as categoryId when creating lessons). Requires Firebase auth.",
|
|
8634
8908
|
{
|
|
8635
|
-
title:
|
|
8636
|
-
productId:
|
|
8637
|
-
description:
|
|
8638
|
-
visibility:
|
|
8639
|
-
sequenceNo:
|
|
8640
|
-
dripDays:
|
|
8641
|
-
locationId:
|
|
8909
|
+
title: import_zod47.z.string().describe("Category title (e.g. 'Module 1')."),
|
|
8910
|
+
productId: import_zod47.z.string().describe("The course/product id this category belongs to."),
|
|
8911
|
+
description: import_zod47.z.string().optional().describe("Category description. Defaults to empty."),
|
|
8912
|
+
visibility: import_zod47.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
|
|
8913
|
+
sequenceNo: import_zod47.z.number().optional().describe("Display order within the course. Defaults to 0."),
|
|
8914
|
+
dripDays: import_zod47.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
|
|
8915
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8642
8916
|
},
|
|
8643
8917
|
async ({ title, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
|
|
8644
8918
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8650,14 +8924,14 @@ ${text2}`);
|
|
|
8650
8924
|
"create_membership_lesson",
|
|
8651
8925
|
"Create a lesson (a 'post') inside a membership course category. Needs both the categoryId (from create_membership_category) and the productId of the course. Description is the lesson body as HTML. Returns the new lesson, including its id. Requires Firebase auth.",
|
|
8652
8926
|
{
|
|
8653
|
-
title:
|
|
8654
|
-
categoryId:
|
|
8655
|
-
productId:
|
|
8656
|
-
description:
|
|
8657
|
-
contentType:
|
|
8658
|
-
visibility:
|
|
8659
|
-
sequenceNo:
|
|
8660
|
-
locationId:
|
|
8927
|
+
title: import_zod47.z.string().describe("Lesson title."),
|
|
8928
|
+
categoryId: import_zod47.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
|
|
8929
|
+
productId: import_zod47.z.string().describe("The course/product id this lesson belongs to."),
|
|
8930
|
+
description: import_zod47.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
|
|
8931
|
+
contentType: import_zod47.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
|
|
8932
|
+
visibility: import_zod47.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
|
|
8933
|
+
sequenceNo: import_zod47.z.number().optional().describe("Display order within the category. Defaults to 0."),
|
|
8934
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8661
8935
|
},
|
|
8662
8936
|
async ({ title, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
|
|
8663
8937
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8669,12 +8943,12 @@ ${text2}`);
|
|
|
8669
8943
|
"create_membership_offer",
|
|
8670
8944
|
"Create a membership offer \u2014 the access grant that enrolls contacts into one or more courses/products. Link it to course product ids. Defaults to a free offer; for paid, set type to 'recurring' or 'one_time' with an amount. Returns the new offer, including its id (referenced by the offer_access_granted trigger). Requires Firebase auth.",
|
|
8671
8945
|
{
|
|
8672
|
-
title:
|
|
8673
|
-
productIds:
|
|
8674
|
-
type:
|
|
8675
|
-
amount:
|
|
8676
|
-
currency:
|
|
8677
|
-
locationId:
|
|
8946
|
+
title: import_zod47.z.string().describe("Offer title (shown at checkout / in the offer list)."),
|
|
8947
|
+
productIds: import_zod47.z.array(import_zod47.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
|
|
8948
|
+
type: import_zod47.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
|
|
8949
|
+
amount: import_zod47.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
|
|
8950
|
+
currency: import_zod47.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
|
|
8951
|
+
locationId: import_zod47.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
8678
8952
|
},
|
|
8679
8953
|
async ({ title, productIds, type, amount, currency, locationId: locationId2 }) => {
|
|
8680
8954
|
const loc = locationId2 ?? client.locationId;
|
|
@@ -8733,60 +9007,60 @@ function buildOfferPayload(o) {
|
|
|
8733
9007
|
}
|
|
8734
9008
|
|
|
8735
9009
|
// src/tools/template-deployer.ts
|
|
8736
|
-
var
|
|
8737
|
-
var
|
|
8738
|
-
var
|
|
9010
|
+
var import_zod48 = require("zod");
|
|
9011
|
+
var fs5 = __toESM(require("fs"));
|
|
9012
|
+
var path5 = __toESM(require("path"));
|
|
8739
9013
|
function delay3(ms) {
|
|
8740
9014
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
8741
9015
|
}
|
|
8742
|
-
var TemplateSchema =
|
|
8743
|
-
templateName:
|
|
8744
|
-
templateVersion:
|
|
8745
|
-
description:
|
|
8746
|
-
questionnaire:
|
|
8747
|
-
id:
|
|
8748
|
-
question:
|
|
8749
|
-
type:
|
|
8750
|
-
required:
|
|
8751
|
-
placeholder:
|
|
9016
|
+
var TemplateSchema = import_zod48.z.object({
|
|
9017
|
+
templateName: import_zod48.z.string(),
|
|
9018
|
+
templateVersion: import_zod48.z.string().optional(),
|
|
9019
|
+
description: import_zod48.z.string().optional().default(""),
|
|
9020
|
+
questionnaire: import_zod48.z.array(import_zod48.z.object({
|
|
9021
|
+
id: import_zod48.z.string(),
|
|
9022
|
+
question: import_zod48.z.string(),
|
|
9023
|
+
type: import_zod48.z.string(),
|
|
9024
|
+
required: import_zod48.z.boolean().optional(),
|
|
9025
|
+
placeholder: import_zod48.z.string().optional()
|
|
8752
9026
|
})).optional().default([]),
|
|
8753
|
-
location:
|
|
8754
|
-
tags:
|
|
8755
|
-
customFields:
|
|
8756
|
-
name:
|
|
8757
|
-
dataType:
|
|
9027
|
+
location: import_zod48.z.record(import_zod48.z.unknown()).optional(),
|
|
9028
|
+
tags: import_zod48.z.array(import_zod48.z.string()).optional(),
|
|
9029
|
+
customFields: import_zod48.z.array(import_zod48.z.object({
|
|
9030
|
+
name: import_zod48.z.string(),
|
|
9031
|
+
dataType: import_zod48.z.string()
|
|
8758
9032
|
})).optional(),
|
|
8759
|
-
pipelines:
|
|
8760
|
-
name:
|
|
8761
|
-
stages:
|
|
9033
|
+
pipelines: import_zod48.z.array(import_zod48.z.object({
|
|
9034
|
+
name: import_zod48.z.string(),
|
|
9035
|
+
stages: import_zod48.z.array(import_zod48.z.object({ position: import_zod48.z.number(), name: import_zod48.z.string() }))
|
|
8762
9036
|
})).optional(),
|
|
8763
|
-
workflows:
|
|
8764
|
-
name:
|
|
8765
|
-
condition:
|
|
8766
|
-
actions:
|
|
9037
|
+
workflows: import_zod48.z.array(import_zod48.z.object({
|
|
9038
|
+
name: import_zod48.z.string(),
|
|
9039
|
+
condition: import_zod48.z.string().optional(),
|
|
9040
|
+
actions: import_zod48.z.array(import_zod48.z.record(import_zod48.z.unknown())).optional().default([])
|
|
8767
9041
|
})).optional(),
|
|
8768
|
-
calendars:
|
|
8769
|
-
name:
|
|
8770
|
-
description:
|
|
9042
|
+
calendars: import_zod48.z.array(import_zod48.z.object({
|
|
9043
|
+
name: import_zod48.z.string(),
|
|
9044
|
+
description: import_zod48.z.string().optional()
|
|
8771
9045
|
})).optional()
|
|
8772
9046
|
});
|
|
8773
9047
|
function registerTemplateDeployerTools(server2, client) {
|
|
8774
9048
|
const builderClient = WorkflowBuilderClient.fromEnv();
|
|
8775
|
-
const templatesDir =
|
|
9049
|
+
const templatesDir = path5.resolve(__dirname, "..", "templates");
|
|
8776
9050
|
function validateTemplatePath(templateFile) {
|
|
8777
|
-
const resolved =
|
|
8778
|
-
const realPath =
|
|
9051
|
+
const resolved = path5.resolve(templateFile);
|
|
9052
|
+
const realPath = fs5.existsSync(resolved) ? fs5.realpathSync(resolved) : resolved;
|
|
8779
9053
|
const allowedDirs = [
|
|
8780
9054
|
templatesDir,
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
].map((d) =>
|
|
8784
|
-
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir +
|
|
9055
|
+
path5.resolve(process.cwd(), "templates"),
|
|
9056
|
+
path5.resolve(__dirname, "..", "..", "templates")
|
|
9057
|
+
].map((d) => fs5.existsSync(d) ? fs5.realpathSync(d) : d);
|
|
9058
|
+
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path5.sep));
|
|
8785
9059
|
if (!isAllowed) {
|
|
8786
9060
|
throw new Error(`Template file must be inside a templates/ directory. Got: ${realPath}`);
|
|
8787
9061
|
}
|
|
8788
|
-
if (
|
|
8789
|
-
const otherMatch = allowedDirs.slice(1).some((d) => !
|
|
9062
|
+
if (path5.relative(allowedDirs[0], realPath).startsWith("..")) {
|
|
9063
|
+
const otherMatch = allowedDirs.slice(1).some((d) => !path5.relative(d, realPath).startsWith(".."));
|
|
8790
9064
|
if (!otherMatch) {
|
|
8791
9065
|
throw new Error(`Template path escapes allowed directory.`);
|
|
8792
9066
|
}
|
|
@@ -8801,19 +9075,19 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
8801
9075
|
try {
|
|
8802
9076
|
const dirs = [
|
|
8803
9077
|
templatesDir,
|
|
8804
|
-
|
|
8805
|
-
|
|
9078
|
+
path5.resolve(process.cwd(), "templates"),
|
|
9079
|
+
path5.resolve(__dirname, "..", "..", "templates")
|
|
8806
9080
|
];
|
|
8807
9081
|
const templates = [];
|
|
8808
9082
|
for (const dir of dirs) {
|
|
8809
|
-
if (!
|
|
8810
|
-
const files =
|
|
9083
|
+
if (!fs5.existsSync(dir)) continue;
|
|
9084
|
+
const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
8811
9085
|
for (const file of files) {
|
|
8812
9086
|
try {
|
|
8813
|
-
const content = JSON.parse(
|
|
9087
|
+
const content = JSON.parse(fs5.readFileSync(path5.join(dir, file), "utf-8"));
|
|
8814
9088
|
templates.push({
|
|
8815
9089
|
name: content.templateName || file,
|
|
8816
|
-
file:
|
|
9090
|
+
file: path5.join(dir, file),
|
|
8817
9091
|
description: content.description || ""
|
|
8818
9092
|
});
|
|
8819
9093
|
} catch {
|
|
@@ -8838,12 +9112,12 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
8838
9112
|
"get_template_questionnaire",
|
|
8839
9113
|
"Get the questionnaire for a specific template. Returns all the questions that need to be answered before deploying. Present these to the user one at a time in a conversational style.",
|
|
8840
9114
|
{
|
|
8841
|
-
templateFile:
|
|
9115
|
+
templateFile: import_zod48.z.string().describe("Path to the template JSON file (from list_templates).")
|
|
8842
9116
|
},
|
|
8843
9117
|
async ({ templateFile }) => {
|
|
8844
9118
|
try {
|
|
8845
9119
|
const safePath = validateTemplatePath(templateFile);
|
|
8846
|
-
const content = JSON.parse(
|
|
9120
|
+
const content = JSON.parse(fs5.readFileSync(safePath, "utf-8"));
|
|
8847
9121
|
return {
|
|
8848
9122
|
content: [
|
|
8849
9123
|
{
|
|
@@ -8871,16 +9145,16 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
8871
9145
|
"deploy_template",
|
|
8872
9146
|
"Deploy a template to set up a GHL sub-account. Creates tags, custom fields, pipelines with stages, calendars, workflows, and forms based on the template and the user's questionnaire answers. This is the main setup automation tool.",
|
|
8873
9147
|
{
|
|
8874
|
-
templateFile:
|
|
8875
|
-
answers:
|
|
8876
|
-
locationId:
|
|
8877
|
-
dryRun:
|
|
9148
|
+
templateFile: import_zod48.z.string().describe("Path to the template JSON file."),
|
|
9149
|
+
answers: import_zod48.z.record(import_zod48.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
|
|
9150
|
+
locationId: import_zod48.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
|
|
9151
|
+
dryRun: import_zod48.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
|
|
8878
9152
|
},
|
|
8879
9153
|
async ({ templateFile, answers, locationId: locationId2, dryRun }) => {
|
|
8880
9154
|
try {
|
|
8881
9155
|
const locId = client.resolveLocationId(locationId2);
|
|
8882
9156
|
const safePath = validateTemplatePath(templateFile);
|
|
8883
|
-
const template = TemplateSchema.parse(JSON.parse(
|
|
9157
|
+
const template = TemplateSchema.parse(JSON.parse(fs5.readFileSync(safePath, "utf-8")));
|
|
8884
9158
|
const resolve5 = (text) => {
|
|
8885
9159
|
if (typeof text !== "string") return text;
|
|
8886
9160
|
let result = text;
|
|
@@ -9120,7 +9394,7 @@ ${errors.join("\n")}` : "\nNo errors!",
|
|
|
9120
9394
|
}
|
|
9121
9395
|
|
|
9122
9396
|
// src/tools/validators.ts
|
|
9123
|
-
var
|
|
9397
|
+
var import_zod49 = require("zod");
|
|
9124
9398
|
var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
|
|
9125
9399
|
var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
|
|
9126
9400
|
"first_name",
|
|
@@ -9578,7 +9852,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
9578
9852
|
server2.tool(
|
|
9579
9853
|
"validate_workflow",
|
|
9580
9854
|
"Pre-flight ID validation for ONE deployed GHL workflow. Scans every trigger and action for references to pipelines, pipeline stages, custom fields, users, workflows, forms, calendars, and surveys; verifies each ID exists in the current location. Use BEFORE publish_workflow when a workflow was edited, or when a published workflow stops behaving. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions. Never reports a false break \u2014 anything it cannot fully verify is marked 'unverified', not 'error'.",
|
|
9581
|
-
{ workflowId:
|
|
9855
|
+
{ workflowId: import_zod49.z.string().describe("The workflow ID to validate.") },
|
|
9582
9856
|
async ({ workflowId }) => {
|
|
9583
9857
|
try {
|
|
9584
9858
|
const workflow = await builderClient.getWorkflow(workflowId);
|
|
@@ -9830,10 +10104,10 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
|
|
|
9830
10104
|
}
|
|
9831
10105
|
|
|
9832
10106
|
// src/tools/snapshots.ts
|
|
9833
|
-
var
|
|
9834
|
-
var SnapshotSchema =
|
|
9835
|
-
var SnapshotsResponseSchema =
|
|
9836
|
-
var ShareLinkResponseSchema =
|
|
10107
|
+
var import_zod50 = require("zod");
|
|
10108
|
+
var SnapshotSchema = import_zod50.z.object({ id: import_zod50.z.string(), name: import_zod50.z.string(), type: import_zod50.z.string() }).passthrough();
|
|
10109
|
+
var SnapshotsResponseSchema = import_zod50.z.object({ snapshots: import_zod50.z.array(SnapshotSchema) }).passthrough();
|
|
10110
|
+
var ShareLinkResponseSchema = import_zod50.z.object({ id: import_zod50.z.string(), shareLink: import_zod50.z.string() }).passthrough();
|
|
9837
10111
|
var SHARE_TYPES = [
|
|
9838
10112
|
"link",
|
|
9839
10113
|
"permanent_link",
|
|
@@ -9880,7 +10154,7 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
9880
10154
|
"list_snapshots",
|
|
9881
10155
|
"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.",
|
|
9882
10156
|
{
|
|
9883
|
-
companyId:
|
|
10157
|
+
companyId: import_zod50.z.string().optional().describe(
|
|
9884
10158
|
"Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
9885
10159
|
)
|
|
9886
10160
|
},
|
|
@@ -9905,11 +10179,11 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
9905
10179
|
"create_snapshot_share_link",
|
|
9906
10180
|
"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.",
|
|
9907
10181
|
{
|
|
9908
|
-
snapshot_id:
|
|
9909
|
-
share_type:
|
|
10182
|
+
snapshot_id: import_zod50.z.string().describe("The snapshot id to share (from list_snapshots)."),
|
|
10183
|
+
share_type: import_zod50.z.enum(SHARE_TYPES).describe(
|
|
9910
10184
|
"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."
|
|
9911
10185
|
),
|
|
9912
|
-
companyId:
|
|
10186
|
+
companyId: import_zod50.z.string().optional().describe(
|
|
9913
10187
|
"Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
9914
10188
|
)
|
|
9915
10189
|
},
|
|
@@ -9939,17 +10213,17 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
9939
10213
|
}
|
|
9940
10214
|
|
|
9941
10215
|
// src/tools/phone.ts
|
|
9942
|
-
var
|
|
9943
|
-
var PhoneNumberSchema =
|
|
9944
|
-
var NumbersResponseSchema =
|
|
9945
|
-
var PoolsResponseSchema =
|
|
10216
|
+
var import_zod51 = require("zod");
|
|
10217
|
+
var PhoneNumberSchema = import_zod51.z.object({ sid: import_zod51.z.string(), value: import_zod51.z.string(), title: import_zod51.z.string().optional() }).passthrough();
|
|
10218
|
+
var NumbersResponseSchema = import_zod51.z.object({ phoneNumbers: import_zod51.z.array(PhoneNumberSchema) }).passthrough();
|
|
10219
|
+
var PoolsResponseSchema = import_zod51.z.object({ pools: import_zod51.z.array(import_zod51.z.object({}).passthrough()) }).passthrough();
|
|
9946
10220
|
function registerPhoneTools(server2, client) {
|
|
9947
10221
|
safeTool(
|
|
9948
10222
|
server2,
|
|
9949
10223
|
"list_phone_numbers",
|
|
9950
10224
|
"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).",
|
|
9951
10225
|
{
|
|
9952
|
-
locationId:
|
|
10226
|
+
locationId: import_zod51.z.string().optional().describe("Defaults to the active location.")
|
|
9953
10227
|
},
|
|
9954
10228
|
async ({ locationId: locationId2 }) => {
|
|
9955
10229
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -9967,7 +10241,7 @@ function registerPhoneTools(server2, client) {
|
|
|
9967
10241
|
"list_number_pools",
|
|
9968
10242
|
"List LC Phone number pools configured for a location. Read-only.",
|
|
9969
10243
|
{
|
|
9970
|
-
locationId:
|
|
10244
|
+
locationId: import_zod51.z.string().optional().describe("Defaults to the active location.")
|
|
9971
10245
|
},
|
|
9972
10246
|
async ({ locationId: locationId2 }) => {
|
|
9973
10247
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -9979,10 +10253,10 @@ function registerPhoneTools(server2, client) {
|
|
|
9979
10253
|
}
|
|
9980
10254
|
|
|
9981
10255
|
// src/tools/account-health.ts
|
|
9982
|
-
var
|
|
9983
|
-
var MetaTotalSchema =
|
|
9984
|
-
var TotalSchema =
|
|
9985
|
-
var NumbersSchema =
|
|
10256
|
+
var import_zod52 = require("zod");
|
|
10257
|
+
var MetaTotalSchema = import_zod52.z.object({ meta: import_zod52.z.object({ total: import_zod52.z.number() }).passthrough() }).passthrough();
|
|
10258
|
+
var TotalSchema = import_zod52.z.object({ total: import_zod52.z.number() }).passthrough();
|
|
10259
|
+
var NumbersSchema = import_zod52.z.object({ phoneNumbers: import_zod52.z.array(import_zod52.z.unknown()) }).passthrough();
|
|
9986
10260
|
var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
|
|
9987
10261
|
async function section(scope, fn) {
|
|
9988
10262
|
try {
|
|
@@ -9997,8 +10271,8 @@ function registerAccountHealthTools(server2, client) {
|
|
|
9997
10271
|
"get_account_health_summary",
|
|
9998
10272
|
"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).",
|
|
9999
10273
|
{
|
|
10000
|
-
locationId:
|
|
10001
|
-
windowDays:
|
|
10274
|
+
locationId: import_zod52.z.string().optional().describe("Defaults to the active location."),
|
|
10275
|
+
windowDays: import_zod52.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
|
|
10002
10276
|
},
|
|
10003
10277
|
async ({ locationId: locationId2, windowDays }) => {
|
|
10004
10278
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -10070,7 +10344,7 @@ function registerAccountHealthTools(server2, client) {
|
|
|
10070
10344
|
}
|
|
10071
10345
|
|
|
10072
10346
|
// src/tools/intake-to-build.ts
|
|
10073
|
-
var
|
|
10347
|
+
var import_zod55 = require("zod");
|
|
10074
10348
|
|
|
10075
10349
|
// src/intake-to-build/question-set.ts
|
|
10076
10350
|
var QUESTION_SET_VERSION = "0.1";
|
|
@@ -10772,7 +11046,7 @@ function buildPlanFormData(fields, locationId2) {
|
|
|
10772
11046
|
}
|
|
10773
11047
|
|
|
10774
11048
|
// src/intake-to-build/brief.ts
|
|
10775
|
-
var
|
|
11049
|
+
var import_zod53 = require("zod");
|
|
10776
11050
|
var BRIEF_SCHEMA_VERSION = "0.1";
|
|
10777
11051
|
var PRESETS = [
|
|
10778
11052
|
"generic",
|
|
@@ -10782,67 +11056,67 @@ var PRESETS = [
|
|
|
10782
11056
|
"ecom",
|
|
10783
11057
|
"agency"
|
|
10784
11058
|
];
|
|
10785
|
-
var presetSchema =
|
|
11059
|
+
var presetSchema = import_zod53.z.enum(PRESETS);
|
|
10786
11060
|
var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
|
|
10787
|
-
var briefSourceSchema =
|
|
10788
|
-
var pricePointSchema =
|
|
10789
|
-
name:
|
|
10790
|
-
price:
|
|
11061
|
+
var briefSourceSchema = import_zod53.z.enum(BRIEF_SOURCES);
|
|
11062
|
+
var pricePointSchema = import_zod53.z.object({
|
|
11063
|
+
name: import_zod53.z.string(),
|
|
11064
|
+
price: import_zod53.z.string()
|
|
10791
11065
|
});
|
|
10792
|
-
var briefSchema =
|
|
10793
|
-
schemaVersion:
|
|
10794
|
-
briefId:
|
|
11066
|
+
var briefSchema = import_zod53.z.object({
|
|
11067
|
+
schemaVersion: import_zod53.z.string(),
|
|
11068
|
+
briefId: import_zod53.z.string(),
|
|
10795
11069
|
preset: presetSchema,
|
|
10796
11070
|
briefSource: briefSourceSchema,
|
|
10797
11071
|
/** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
|
|
10798
|
-
extended:
|
|
10799
|
-
business:
|
|
10800
|
-
name:
|
|
10801
|
-
type:
|
|
10802
|
-
website:
|
|
10803
|
-
location:
|
|
10804
|
-
timezone:
|
|
11072
|
+
extended: import_zod53.z.record(import_zod53.z.unknown()).optional(),
|
|
11073
|
+
business: import_zod53.z.object({
|
|
11074
|
+
name: import_zod53.z.string(),
|
|
11075
|
+
type: import_zod53.z.string().optional(),
|
|
11076
|
+
website: import_zod53.z.string().optional(),
|
|
11077
|
+
location: import_zod53.z.string().optional(),
|
|
11078
|
+
timezone: import_zod53.z.string().optional(),
|
|
10805
11079
|
// Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
|
|
10806
11080
|
// the same tolerance reason as business.type (don't reject valid briefs).
|
|
10807
|
-
teamSize:
|
|
10808
|
-
monthlyLeadVolume:
|
|
10809
|
-
hours:
|
|
11081
|
+
teamSize: import_zod53.z.string().optional(),
|
|
11082
|
+
monthlyLeadVolume: import_zod53.z.string().optional(),
|
|
11083
|
+
hours: import_zod53.z.string().optional()
|
|
10810
11084
|
}).passthrough(),
|
|
10811
|
-
offer:
|
|
10812
|
-
summary:
|
|
11085
|
+
offer: import_zod53.z.object({
|
|
11086
|
+
summary: import_zod53.z.string().optional(),
|
|
10813
11087
|
// Parsed best-effort; tolerate a raw string when parsing was not possible.
|
|
10814
|
-
pricePoints:
|
|
10815
|
-
leadMagnet:
|
|
10816
|
-
avgDealValue:
|
|
11088
|
+
pricePoints: import_zod53.z.union([import_zod53.z.array(pricePointSchema), import_zod53.z.string()]).optional(),
|
|
11089
|
+
leadMagnet: import_zod53.z.string().optional(),
|
|
11090
|
+
avgDealValue: import_zod53.z.string().optional()
|
|
10817
11091
|
}).passthrough().optional(),
|
|
10818
|
-
audience:
|
|
10819
|
-
ideal:
|
|
10820
|
-
painPoints:
|
|
10821
|
-
objections:
|
|
11092
|
+
audience: import_zod53.z.object({
|
|
11093
|
+
ideal: import_zod53.z.string().optional(),
|
|
11094
|
+
painPoints: import_zod53.z.array(import_zod53.z.string()).optional(),
|
|
11095
|
+
objections: import_zod53.z.array(import_zod53.z.string()).optional()
|
|
10822
11096
|
}).passthrough().optional(),
|
|
10823
|
-
goal:
|
|
11097
|
+
goal: import_zod53.z.object({
|
|
10824
11098
|
// Kept as string: the form option labels are the canonical values, but
|
|
10825
11099
|
// the contract sample shortens some (e.g. "high-touch"). See §7 note.
|
|
10826
|
-
primary:
|
|
10827
|
-
salesStages:
|
|
10828
|
-
bookingNeeded:
|
|
10829
|
-
followUpStyle:
|
|
11100
|
+
primary: import_zod53.z.string(),
|
|
11101
|
+
salesStages: import_zod53.z.array(import_zod53.z.string()).optional(),
|
|
11102
|
+
bookingNeeded: import_zod53.z.boolean().optional(),
|
|
11103
|
+
followUpStyle: import_zod53.z.string().optional()
|
|
10830
11104
|
}).passthrough(),
|
|
10831
|
-
channels:
|
|
10832
|
-
email:
|
|
10833
|
-
sms:
|
|
10834
|
-
a2pStatus:
|
|
10835
|
-
payment:
|
|
10836
|
-
calendarConnected:
|
|
10837
|
-
social:
|
|
11105
|
+
channels: import_zod53.z.object({
|
|
11106
|
+
email: import_zod53.z.boolean().optional(),
|
|
11107
|
+
sms: import_zod53.z.boolean().optional(),
|
|
11108
|
+
a2pStatus: import_zod53.z.string().optional(),
|
|
11109
|
+
payment: import_zod53.z.string().optional(),
|
|
11110
|
+
calendarConnected: import_zod53.z.boolean().optional(),
|
|
11111
|
+
social: import_zod53.z.array(import_zod53.z.string()).optional()
|
|
10838
11112
|
}).passthrough().optional(),
|
|
10839
|
-
assets:
|
|
10840
|
-
existingPipeline:
|
|
10841
|
-
existingWorkflows:
|
|
10842
|
-
brand:
|
|
10843
|
-
notes:
|
|
11113
|
+
assets: import_zod53.z.object({
|
|
11114
|
+
existingPipeline: import_zod53.z.string().optional(),
|
|
11115
|
+
existingWorkflows: import_zod53.z.string().optional(),
|
|
11116
|
+
brand: import_zod53.z.string().optional(),
|
|
11117
|
+
notes: import_zod53.z.string().optional()
|
|
10844
11118
|
}).passthrough().optional(),
|
|
10845
|
-
flags:
|
|
11119
|
+
flags: import_zod53.z.array(import_zod53.z.string()).optional()
|
|
10846
11120
|
}).strict();
|
|
10847
11121
|
function validateBrief(input) {
|
|
10848
11122
|
const parsed = briefSchema.safeParse(input);
|
|
@@ -10934,9 +11208,9 @@ function presetForBusinessType(type) {
|
|
|
10934
11208
|
return "generic";
|
|
10935
11209
|
}
|
|
10936
11210
|
}
|
|
10937
|
-
function setPath(target,
|
|
11211
|
+
function setPath(target, path8, value) {
|
|
10938
11212
|
if (value === void 0) return;
|
|
10939
|
-
const parts =
|
|
11213
|
+
const parts = path8.split(".");
|
|
10940
11214
|
let node = target;
|
|
10941
11215
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
10942
11216
|
const k = parts[i];
|
|
@@ -10998,7 +11272,7 @@ function normalizeSubmissionToBrief(opts) {
|
|
|
10998
11272
|
}
|
|
10999
11273
|
|
|
11000
11274
|
// src/intake-to-build/plan.ts
|
|
11001
|
-
var
|
|
11275
|
+
var import_zod54 = require("zod");
|
|
11002
11276
|
var REF_NAMESPACES = [
|
|
11003
11277
|
"pipeline",
|
|
11004
11278
|
"stage",
|
|
@@ -11015,22 +11289,22 @@ var REF_NAMESPACES = [
|
|
|
11015
11289
|
"handoff"
|
|
11016
11290
|
];
|
|
11017
11291
|
var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
|
|
11018
|
-
var refSchema =
|
|
11292
|
+
var refSchema = import_zod54.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
|
|
11019
11293
|
function nsRef(ns) {
|
|
11020
|
-
return
|
|
11294
|
+
return import_zod54.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
|
|
11021
11295
|
}
|
|
11022
11296
|
function refNamespace(ref) {
|
|
11023
11297
|
return ref.split(".")[0];
|
|
11024
11298
|
}
|
|
11025
|
-
var stageSchema =
|
|
11299
|
+
var stageSchema = import_zod54.z.object({
|
|
11026
11300
|
ref: nsRef("stage"),
|
|
11027
|
-
name:
|
|
11028
|
-
position:
|
|
11301
|
+
name: import_zod54.z.string(),
|
|
11302
|
+
position: import_zod54.z.number().int().nonnegative()
|
|
11029
11303
|
});
|
|
11030
|
-
var pipelineSchema =
|
|
11304
|
+
var pipelineSchema = import_zod54.z.object({
|
|
11031
11305
|
ref: nsRef("pipeline"),
|
|
11032
|
-
name:
|
|
11033
|
-
stages:
|
|
11306
|
+
name: import_zod54.z.string(),
|
|
11307
|
+
stages: import_zod54.z.array(stageSchema).min(1)
|
|
11034
11308
|
});
|
|
11035
11309
|
var GHL_FIELD_DATATYPES = [
|
|
11036
11310
|
"TEXT",
|
|
@@ -11047,21 +11321,21 @@ var GHL_FIELD_DATATYPES = [
|
|
|
11047
11321
|
"FILE_UPLOAD",
|
|
11048
11322
|
"SIGNATURE"
|
|
11049
11323
|
];
|
|
11050
|
-
var customFieldSchema =
|
|
11324
|
+
var customFieldSchema = import_zod54.z.object({
|
|
11051
11325
|
ref: nsRef("field"),
|
|
11052
|
-
name:
|
|
11053
|
-
dataType:
|
|
11054
|
-
model:
|
|
11055
|
-
options:
|
|
11326
|
+
name: import_zod54.z.string(),
|
|
11327
|
+
dataType: import_zod54.z.enum(GHL_FIELD_DATATYPES),
|
|
11328
|
+
model: import_zod54.z.enum(["contact", "opportunity"]).optional(),
|
|
11329
|
+
options: import_zod54.z.array(import_zod54.z.string()).optional()
|
|
11056
11330
|
});
|
|
11057
|
-
var tagSchema =
|
|
11331
|
+
var tagSchema = import_zod54.z.object({
|
|
11058
11332
|
ref: nsRef("tag"),
|
|
11059
|
-
name:
|
|
11333
|
+
name: import_zod54.z.string()
|
|
11060
11334
|
});
|
|
11061
|
-
var customValueSchema =
|
|
11335
|
+
var customValueSchema = import_zod54.z.object({
|
|
11062
11336
|
ref: nsRef("cv"),
|
|
11063
|
-
name:
|
|
11064
|
-
value:
|
|
11337
|
+
name: import_zod54.z.string(),
|
|
11338
|
+
value: import_zod54.z.string().optional(),
|
|
11065
11339
|
filledBy: refSchema.optional()
|
|
11066
11340
|
});
|
|
11067
11341
|
var CALENDAR_TYPES = [
|
|
@@ -11071,89 +11345,89 @@ var CALENDAR_TYPES = [
|
|
|
11071
11345
|
"collective",
|
|
11072
11346
|
"service_booking"
|
|
11073
11347
|
];
|
|
11074
|
-
var openHoursBlockSchema =
|
|
11075
|
-
daysOfTheWeek:
|
|
11076
|
-
hours:
|
|
11077
|
-
|
|
11078
|
-
openHour:
|
|
11079
|
-
openMinute:
|
|
11080
|
-
closeHour:
|
|
11081
|
-
closeMinute:
|
|
11348
|
+
var openHoursBlockSchema = import_zod54.z.object({
|
|
11349
|
+
daysOfTheWeek: import_zod54.z.array(import_zod54.z.number().int().min(0).max(6)),
|
|
11350
|
+
hours: import_zod54.z.array(
|
|
11351
|
+
import_zod54.z.object({
|
|
11352
|
+
openHour: import_zod54.z.number().int().min(0).max(23),
|
|
11353
|
+
openMinute: import_zod54.z.number().int().min(0).max(59),
|
|
11354
|
+
closeHour: import_zod54.z.number().int().min(0).max(23),
|
|
11355
|
+
closeMinute: import_zod54.z.number().int().min(0).max(59)
|
|
11082
11356
|
})
|
|
11083
11357
|
)
|
|
11084
11358
|
});
|
|
11085
|
-
var calendarSchema =
|
|
11359
|
+
var calendarSchema = import_zod54.z.object({
|
|
11086
11360
|
ref: nsRef("calendar"),
|
|
11087
|
-
name:
|
|
11088
|
-
calendarType:
|
|
11089
|
-
openHours:
|
|
11090
|
-
availabilityType:
|
|
11091
|
-
requiresStaff:
|
|
11361
|
+
name: import_zod54.z.string(),
|
|
11362
|
+
calendarType: import_zod54.z.enum(CALENDAR_TYPES),
|
|
11363
|
+
openHours: import_zod54.z.array(openHoursBlockSchema).optional(),
|
|
11364
|
+
availabilityType: import_zod54.z.number().int().optional(),
|
|
11365
|
+
requiresStaff: import_zod54.z.boolean().optional()
|
|
11092
11366
|
});
|
|
11093
|
-
var formFieldSchema =
|
|
11094
|
-
|
|
11095
|
-
type:
|
|
11096
|
-
key:
|
|
11097
|
-
required:
|
|
11367
|
+
var formFieldSchema = import_zod54.z.discriminatedUnion("type", [
|
|
11368
|
+
import_zod54.z.object({
|
|
11369
|
+
type: import_zod54.z.literal("standard"),
|
|
11370
|
+
key: import_zod54.z.string(),
|
|
11371
|
+
required: import_zod54.z.boolean().optional()
|
|
11098
11372
|
}),
|
|
11099
|
-
|
|
11100
|
-
type:
|
|
11373
|
+
import_zod54.z.object({
|
|
11374
|
+
type: import_zod54.z.literal("custom"),
|
|
11101
11375
|
fieldRef: nsRef("field"),
|
|
11102
|
-
required:
|
|
11376
|
+
required: import_zod54.z.boolean().optional()
|
|
11103
11377
|
})
|
|
11104
11378
|
]);
|
|
11105
|
-
var formSchema =
|
|
11379
|
+
var formSchema = import_zod54.z.object({
|
|
11106
11380
|
ref: nsRef("form"),
|
|
11107
|
-
name:
|
|
11108
|
-
fields:
|
|
11381
|
+
name: import_zod54.z.string(),
|
|
11382
|
+
fields: import_zod54.z.array(formFieldSchema)
|
|
11109
11383
|
});
|
|
11110
|
-
var pageSchema =
|
|
11384
|
+
var pageSchema = import_zod54.z.object({
|
|
11111
11385
|
ref: nsRef("page"),
|
|
11112
|
-
name:
|
|
11113
|
-
role:
|
|
11114
|
-
outline:
|
|
11386
|
+
name: import_zod54.z.string(),
|
|
11387
|
+
role: import_zod54.z.string().optional(),
|
|
11388
|
+
outline: import_zod54.z.string().optional(),
|
|
11115
11389
|
formRef: nsRef("form").optional(),
|
|
11116
11390
|
calendarRef: nsRef("calendar").optional()
|
|
11117
11391
|
});
|
|
11118
11392
|
var FUNNEL_TARGETS = ["ghl", "external"];
|
|
11119
11393
|
var FUNNEL_HOSTS = ["cloudflare", "vercel"];
|
|
11120
|
-
var funnelSchema =
|
|
11394
|
+
var funnelSchema = import_zod54.z.object({
|
|
11121
11395
|
ref: nsRef("funnel"),
|
|
11122
|
-
name:
|
|
11396
|
+
name: import_zod54.z.string(),
|
|
11123
11397
|
// Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
|
|
11124
11398
|
// "external" = the subscriber builds + hosts the site themselves (Cloudflare/
|
|
11125
11399
|
// Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
|
|
11126
11400
|
// see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
|
|
11127
11401
|
// deploy an external funnel; it surfaces the GHL-side wiring info.
|
|
11128
|
-
target:
|
|
11129
|
-
host:
|
|
11402
|
+
target: import_zod54.z.enum(FUNNEL_TARGETS).optional(),
|
|
11403
|
+
host: import_zod54.z.enum(FUNNEL_HOSTS).optional(),
|
|
11130
11404
|
// external only
|
|
11131
|
-
domain:
|
|
11405
|
+
domain: import_zod54.z.string().optional(),
|
|
11132
11406
|
// external only
|
|
11133
|
-
pages:
|
|
11407
|
+
pages: import_zod54.z.array(pageSchema)
|
|
11134
11408
|
});
|
|
11135
|
-
var emailAssetSchema =
|
|
11409
|
+
var emailAssetSchema = import_zod54.z.object({
|
|
11136
11410
|
ref: nsRef("email"),
|
|
11137
|
-
name:
|
|
11138
|
-
subject:
|
|
11139
|
-
bodyOutline:
|
|
11140
|
-
body:
|
|
11141
|
-
mergeTags:
|
|
11411
|
+
name: import_zod54.z.string(),
|
|
11412
|
+
subject: import_zod54.z.string().optional(),
|
|
11413
|
+
bodyOutline: import_zod54.z.string().optional(),
|
|
11414
|
+
body: import_zod54.z.string().optional(),
|
|
11415
|
+
mergeTags: import_zod54.z.array(import_zod54.z.string()).optional()
|
|
11142
11416
|
});
|
|
11143
|
-
var smsAssetSchema =
|
|
11417
|
+
var smsAssetSchema = import_zod54.z.object({
|
|
11144
11418
|
ref: nsRef("sms"),
|
|
11145
|
-
name:
|
|
11146
|
-
bodyOutline:
|
|
11147
|
-
body:
|
|
11148
|
-
mergeTags:
|
|
11419
|
+
name: import_zod54.z.string(),
|
|
11420
|
+
bodyOutline: import_zod54.z.string().optional(),
|
|
11421
|
+
body: import_zod54.z.string().optional(),
|
|
11422
|
+
mergeTags: import_zod54.z.array(import_zod54.z.string()).optional()
|
|
11149
11423
|
});
|
|
11150
|
-
var waitUnit =
|
|
11424
|
+
var waitUnit = import_zod54.z.enum(["minutes", "hours", "days"]);
|
|
11151
11425
|
var branchActionOptions = [
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11426
|
+
import_zod54.z.object({ type: import_zod54.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
|
|
11427
|
+
import_zod54.z.object({ type: import_zod54.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
|
|
11428
|
+
import_zod54.z.object({ type: import_zod54.z.literal("send_email"), emailRef: nsRef("email") }),
|
|
11429
|
+
import_zod54.z.object({ type: import_zod54.z.literal("send_sms"), smsRef: nsRef("sms") }),
|
|
11430
|
+
import_zod54.z.object({ type: import_zod54.z.literal("wait"), value: import_zod54.z.number().positive(), unit: waitUnit }),
|
|
11157
11431
|
// Appointment-relative wait ("wait until N BEFORE the appointment").
|
|
11158
11432
|
// Only works when the workflow has an appointment in context (i.e. an
|
|
11159
11433
|
// `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
|
|
@@ -11162,85 +11436,85 @@ var branchActionOptions = [
|
|
|
11162
11436
|
// (GHL stores whole minutes). Only "before" is emitted today — that's the
|
|
11163
11437
|
// shape we captured + proved; "after" (post-appointment follow-up) is
|
|
11164
11438
|
// deferred until its shape is captured from a real workflow.
|
|
11165
|
-
|
|
11166
|
-
type:
|
|
11167
|
-
value:
|
|
11439
|
+
import_zod54.z.object({
|
|
11440
|
+
type: import_zod54.z.literal("wait_appointment"),
|
|
11441
|
+
value: import_zod54.z.number().int().positive(),
|
|
11168
11442
|
unit: waitUnit
|
|
11169
11443
|
}),
|
|
11170
|
-
|
|
11171
|
-
type:
|
|
11172
|
-
to:
|
|
11173
|
-
title:
|
|
11174
|
-
body:
|
|
11444
|
+
import_zod54.z.object({
|
|
11445
|
+
type: import_zod54.z.literal("internal_notification"),
|
|
11446
|
+
to: import_zod54.z.string(),
|
|
11447
|
+
title: import_zod54.z.string(),
|
|
11448
|
+
body: import_zod54.z.string()
|
|
11175
11449
|
}),
|
|
11176
|
-
|
|
11177
|
-
type:
|
|
11450
|
+
import_zod54.z.object({
|
|
11451
|
+
type: import_zod54.z.literal("update_contact_field"),
|
|
11178
11452
|
fieldRef: nsRef("field"),
|
|
11179
|
-
value:
|
|
11453
|
+
value: import_zod54.z.string()
|
|
11180
11454
|
}),
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
type:
|
|
11184
|
-
title:
|
|
11185
|
-
body:
|
|
11186
|
-
dueDate:
|
|
11187
|
-
assignedTo:
|
|
11455
|
+
import_zod54.z.object({ type: import_zod54.z.literal("add_notes"), body: import_zod54.z.string() }),
|
|
11456
|
+
import_zod54.z.object({
|
|
11457
|
+
type: import_zod54.z.literal("task_notification"),
|
|
11458
|
+
title: import_zod54.z.string(),
|
|
11459
|
+
body: import_zod54.z.string().optional(),
|
|
11460
|
+
dueDate: import_zod54.z.string().optional(),
|
|
11461
|
+
assignedTo: import_zod54.z.string().optional()
|
|
11188
11462
|
}),
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
type:
|
|
11463
|
+
import_zod54.z.object({ type: import_zod54.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
|
|
11464
|
+
import_zod54.z.object({ type: import_zod54.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
|
|
11465
|
+
import_zod54.z.object({
|
|
11466
|
+
type: import_zod54.z.literal("create_opportunity"),
|
|
11193
11467
|
pipelineRef: nsRef("pipeline"),
|
|
11194
11468
|
stageRef: nsRef("stage"),
|
|
11195
11469
|
// Opportunity name (merge fields allowed). Defaults to the contact's name.
|
|
11196
11470
|
// Required by GHL's create node; without it the create silently no-ops.
|
|
11197
|
-
name:
|
|
11471
|
+
name: import_zod54.z.string().optional(),
|
|
11198
11472
|
// Opportunity monetary value (the deal/sale dollar amount). A string so it
|
|
11199
11473
|
// can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
|
|
11200
11474
|
// Optional — omitted → GHL leaves the value unset. Shape captured from Lux
|
|
11201
11475
|
// Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
|
|
11202
11476
|
// won/lost status, so a "won" opp = move to the closing stage WITH this value.
|
|
11203
|
-
value:
|
|
11477
|
+
value: import_zod54.z.string().optional()
|
|
11204
11478
|
}),
|
|
11205
|
-
|
|
11206
|
-
type:
|
|
11479
|
+
import_zod54.z.object({
|
|
11480
|
+
type: import_zod54.z.literal("update_opportunity"),
|
|
11207
11481
|
pipelineRef: nsRef("pipeline"),
|
|
11208
11482
|
stageRef: nsRef("stage"),
|
|
11209
11483
|
// Opportunity monetary value (see create_opportunity.value). Optional.
|
|
11210
|
-
value:
|
|
11484
|
+
value: import_zod54.z.string().optional()
|
|
11211
11485
|
}),
|
|
11212
|
-
|
|
11213
|
-
type:
|
|
11214
|
-
goalCondition:
|
|
11486
|
+
import_zod54.z.object({
|
|
11487
|
+
type: import_zod54.z.literal("goal_event"),
|
|
11488
|
+
goalCondition: import_zod54.z.string(),
|
|
11215
11489
|
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
11216
|
-
action:
|
|
11490
|
+
action: import_zod54.z.enum(["exit", "continue", "wait"]).optional()
|
|
11217
11491
|
})
|
|
11218
11492
|
];
|
|
11219
|
-
var branchActionSchema =
|
|
11220
|
-
var findOpportunitySchema =
|
|
11221
|
-
type:
|
|
11493
|
+
var branchActionSchema = import_zod54.z.discriminatedUnion("type", branchActionOptions);
|
|
11494
|
+
var findOpportunitySchema = import_zod54.z.object({
|
|
11495
|
+
type: import_zod54.z.literal("find_opportunity"),
|
|
11222
11496
|
pipelineRef: nsRef("pipeline"),
|
|
11223
|
-
found:
|
|
11224
|
-
notFound:
|
|
11497
|
+
found: import_zod54.z.array(branchActionSchema).default([]),
|
|
11498
|
+
notFound: import_zod54.z.array(branchActionSchema).default([])
|
|
11225
11499
|
});
|
|
11226
|
-
var actionSchema =
|
|
11500
|
+
var actionSchema = import_zod54.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
11227
11501
|
var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
11228
|
-
var triggerSchema =
|
|
11229
|
-
type:
|
|
11502
|
+
var triggerSchema = import_zod54.z.object({
|
|
11503
|
+
type: import_zod54.z.string(),
|
|
11230
11504
|
formRef: nsRef("form").optional(),
|
|
11231
11505
|
tagRef: nsRef("tag").optional(),
|
|
11232
11506
|
calendarRef: nsRef("calendar").optional(),
|
|
11233
11507
|
pipelineRef: nsRef("pipeline").optional(),
|
|
11234
11508
|
stageRef: nsRef("stage").optional(),
|
|
11235
11509
|
// Required for a native `appointment` trigger (the status it fires on).
|
|
11236
|
-
appointmentStatus:
|
|
11510
|
+
appointmentStatus: import_zod54.z.enum(APPOINTMENT_STATUSES).optional()
|
|
11237
11511
|
});
|
|
11238
|
-
var workflowSchema =
|
|
11512
|
+
var workflowSchema = import_zod54.z.object({
|
|
11239
11513
|
ref: nsRef("workflow"),
|
|
11240
|
-
name:
|
|
11514
|
+
name: import_zod54.z.string(),
|
|
11241
11515
|
trigger: triggerSchema.optional(),
|
|
11242
|
-
stopOnResponse:
|
|
11243
|
-
actions:
|
|
11516
|
+
stopOnResponse: import_zod54.z.boolean().optional(),
|
|
11517
|
+
actions: import_zod54.z.array(actionSchema).max(40)
|
|
11244
11518
|
// house rule: <=40 actions/workflow
|
|
11245
11519
|
});
|
|
11246
11520
|
var HANDOFF_OWNER_LEGACY = {
|
|
@@ -11248,35 +11522,35 @@ var HANDOFF_OWNER_LEGACY = {
|
|
|
11248
11522
|
"JERRY-EXT": "OPERATOR-EXT",
|
|
11249
11523
|
"SASHA": "TEAM"
|
|
11250
11524
|
};
|
|
11251
|
-
var handoffSchema =
|
|
11525
|
+
var handoffSchema = import_zod54.z.object({
|
|
11252
11526
|
ref: nsRef("handoff"),
|
|
11253
|
-
owner:
|
|
11254
|
-
title:
|
|
11255
|
-
trigger:
|
|
11256
|
-
instruction:
|
|
11527
|
+
owner: import_zod54.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
|
|
11528
|
+
title: import_zod54.z.string(),
|
|
11529
|
+
trigger: import_zod54.z.string().optional(),
|
|
11530
|
+
instruction: import_zod54.z.string(),
|
|
11257
11531
|
produces: refSchema.nullable().optional(),
|
|
11258
|
-
successCheck:
|
|
11259
|
-
blocks:
|
|
11532
|
+
successCheck: import_zod54.z.string(),
|
|
11533
|
+
blocks: import_zod54.z.array(import_zod54.z.string()).optional()
|
|
11260
11534
|
});
|
|
11261
|
-
var buildPlanSchema =
|
|
11262
|
-
schemaVersion:
|
|
11263
|
-
planId:
|
|
11264
|
-
briefId:
|
|
11265
|
-
preset:
|
|
11266
|
-
summary:
|
|
11267
|
-
pipelines:
|
|
11268
|
-
customFields:
|
|
11269
|
-
tags:
|
|
11270
|
-
customValues:
|
|
11271
|
-
calendars:
|
|
11272
|
-
forms:
|
|
11273
|
-
funnels:
|
|
11274
|
-
emails:
|
|
11275
|
-
sms:
|
|
11276
|
-
workflows:
|
|
11277
|
-
handoffs:
|
|
11278
|
-
buildOrder:
|
|
11279
|
-
idMap:
|
|
11535
|
+
var buildPlanSchema = import_zod54.z.object({
|
|
11536
|
+
schemaVersion: import_zod54.z.string(),
|
|
11537
|
+
planId: import_zod54.z.string(),
|
|
11538
|
+
briefId: import_zod54.z.string(),
|
|
11539
|
+
preset: import_zod54.z.string(),
|
|
11540
|
+
summary: import_zod54.z.string().optional(),
|
|
11541
|
+
pipelines: import_zod54.z.array(pipelineSchema).optional(),
|
|
11542
|
+
customFields: import_zod54.z.array(customFieldSchema).optional(),
|
|
11543
|
+
tags: import_zod54.z.array(tagSchema).optional(),
|
|
11544
|
+
customValues: import_zod54.z.array(customValueSchema).optional(),
|
|
11545
|
+
calendars: import_zod54.z.array(calendarSchema).optional(),
|
|
11546
|
+
forms: import_zod54.z.array(formSchema).optional(),
|
|
11547
|
+
funnels: import_zod54.z.array(funnelSchema).optional(),
|
|
11548
|
+
emails: import_zod54.z.array(emailAssetSchema).optional(),
|
|
11549
|
+
sms: import_zod54.z.array(smsAssetSchema).optional(),
|
|
11550
|
+
workflows: import_zod54.z.array(workflowSchema).optional(),
|
|
11551
|
+
handoffs: import_zod54.z.array(handoffSchema).optional(),
|
|
11552
|
+
buildOrder: import_zod54.z.array(import_zod54.z.string()).optional(),
|
|
11553
|
+
idMap: import_zod54.z.record(import_zod54.z.string()).optional()
|
|
11280
11554
|
}).strict();
|
|
11281
11555
|
function collectDefinedRefs(plan) {
|
|
11282
11556
|
const refs = /* @__PURE__ */ new Map();
|
|
@@ -12410,7 +12684,7 @@ function buildExternalWiring(plan, idMap, locationId2) {
|
|
|
12410
12684
|
|
|
12411
12685
|
// src/intake-to-build/execute.ts
|
|
12412
12686
|
var norm2 = (s) => s.trim().toLowerCase();
|
|
12413
|
-
var
|
|
12687
|
+
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12414
12688
|
function slugifyName(s) {
|
|
12415
12689
|
return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
|
|
12416
12690
|
}
|
|
@@ -12435,7 +12709,7 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
12435
12709
|
const fresh = (await read()).filter((o) => norm2(o.name) === norm2(name) && !beforeIds.has(o.id));
|
|
12436
12710
|
if (fresh.length === 1) return fresh[0];
|
|
12437
12711
|
if (fresh.length > 1) return void 0;
|
|
12438
|
-
if (attempt < retries) await
|
|
12712
|
+
if (attempt < retries) await sleep3(backoff * attempt);
|
|
12439
12713
|
}
|
|
12440
12714
|
return void 0;
|
|
12441
12715
|
}
|
|
@@ -12861,16 +13135,16 @@ function msg(e) {
|
|
|
12861
13135
|
}
|
|
12862
13136
|
|
|
12863
13137
|
// src/tools/intake-to-build.ts
|
|
12864
|
-
var customFieldItemSchema =
|
|
12865
|
-
id:
|
|
12866
|
-
name:
|
|
12867
|
-
fieldKey:
|
|
12868
|
-
dataType:
|
|
12869
|
-
model:
|
|
12870
|
-
parentId:
|
|
12871
|
-
position:
|
|
12872
|
-
dateAdded:
|
|
12873
|
-
picklistOptions:
|
|
13138
|
+
var customFieldItemSchema = import_zod55.z.object({
|
|
13139
|
+
id: import_zod55.z.string(),
|
|
13140
|
+
name: import_zod55.z.string(),
|
|
13141
|
+
fieldKey: import_zod55.z.string(),
|
|
13142
|
+
dataType: import_zod55.z.string(),
|
|
13143
|
+
model: import_zod55.z.string().optional(),
|
|
13144
|
+
parentId: import_zod55.z.string().optional(),
|
|
13145
|
+
position: import_zod55.z.number().optional(),
|
|
13146
|
+
dateAdded: import_zod55.z.string().optional(),
|
|
13147
|
+
picklistOptions: import_zod55.z.array(import_zod55.z.string()).optional()
|
|
12874
13148
|
}).passthrough();
|
|
12875
13149
|
function parseCustomFields(raw) {
|
|
12876
13150
|
const obj = raw && typeof raw === "object" ? raw : {};
|
|
@@ -12917,7 +13191,7 @@ function findRecordForQuestion(q, records) {
|
|
|
12917
13191
|
const wantName = intakeFieldName(q.label).toLowerCase();
|
|
12918
13192
|
return records.find((r) => r.name.toLowerCase() === wantName);
|
|
12919
13193
|
}
|
|
12920
|
-
var
|
|
13194
|
+
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12921
13195
|
function isFormNotYetPropagated(error) {
|
|
12922
13196
|
const msg2 = error instanceof Error ? error.message : String(error);
|
|
12923
13197
|
return /does not exist or is deleted/i.test(msg2);
|
|
@@ -13022,15 +13296,15 @@ function extractFunnelId(result) {
|
|
|
13022
13296
|
return void 0;
|
|
13023
13297
|
}
|
|
13024
13298
|
function makeExecuteDeps(client, builderClient, locationId2) {
|
|
13025
|
-
const pipelineApi = async (method,
|
|
13299
|
+
const pipelineApi = async (method, path8, body) => {
|
|
13026
13300
|
const headers = await builderClient.buildHeaders();
|
|
13027
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
13301
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path8}`;
|
|
13028
13302
|
const options = { method, headers };
|
|
13029
13303
|
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
13030
13304
|
const response = await fetch(url, options);
|
|
13031
13305
|
if (!response.ok) {
|
|
13032
13306
|
const text2 = await response.text();
|
|
13033
|
-
throw new Error(`Pipeline API ${response.status}: ${method} ${
|
|
13307
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path8}
|
|
13034
13308
|
${text2.slice(0, 300)}`);
|
|
13035
13309
|
}
|
|
13036
13310
|
const text = await response.text();
|
|
@@ -13041,17 +13315,17 @@ ${text2.slice(0, 300)}`);
|
|
|
13041
13315
|
return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
|
|
13042
13316
|
}
|
|
13043
13317
|
};
|
|
13044
|
-
const funnelApi = async (method,
|
|
13318
|
+
const funnelApi = async (method, path8, body) => {
|
|
13045
13319
|
const headers = await builderClient.buildHeaders();
|
|
13046
13320
|
headers.Origin = "https://app.gohighlevel.com";
|
|
13047
13321
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
13048
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
13322
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path8}`;
|
|
13049
13323
|
const options = { method, headers };
|
|
13050
13324
|
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
13051
13325
|
const response = await fetch(url, options);
|
|
13052
13326
|
if (!response.ok) {
|
|
13053
13327
|
const text2 = await response.text();
|
|
13054
|
-
throw new Error(`Funnel API ${response.status}: ${method} ${
|
|
13328
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path8}
|
|
13055
13329
|
${text2.slice(0, 300)}`);
|
|
13056
13330
|
}
|
|
13057
13331
|
const text = await response.text();
|
|
@@ -13137,7 +13411,7 @@ ${text2.slice(0, 300)}`);
|
|
|
13137
13411
|
break;
|
|
13138
13412
|
} catch (saveErr) {
|
|
13139
13413
|
if (isFormNotYetPropagated(saveErr) && attempt < 6) {
|
|
13140
|
-
await
|
|
13414
|
+
await sleep4(700 * attempt);
|
|
13141
13415
|
continue;
|
|
13142
13416
|
}
|
|
13143
13417
|
throw saveErr;
|
|
@@ -13148,7 +13422,7 @@ ${text2.slice(0, 300)}`);
|
|
|
13148
13422
|
const verify = await formApiRequest(builderClient, "GET", `/${formId}?locationId=${locationId2}`);
|
|
13149
13423
|
persisted = countFormFields(verify);
|
|
13150
13424
|
if (persisted > 0) break;
|
|
13151
|
-
if (attempt < 6) await
|
|
13425
|
+
if (attempt < 6) await sleep4(700 * attempt);
|
|
13152
13426
|
}
|
|
13153
13427
|
if (persisted === 0) {
|
|
13154
13428
|
throw new Error(`form "${name}" was created (${formId}) but no fields persisted after save (read-after-write); not binding`);
|
|
@@ -13279,7 +13553,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13279
13553
|
"validate_brief",
|
|
13280
13554
|
"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.",
|
|
13281
13555
|
{
|
|
13282
|
-
brief:
|
|
13556
|
+
brief: import_zod55.z.record(import_zod55.z.unknown()).describe("The Brief object to validate.")
|
|
13283
13557
|
},
|
|
13284
13558
|
async ({ brief }) => validateBrief(brief)
|
|
13285
13559
|
);
|
|
@@ -13288,7 +13562,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13288
13562
|
"validate_build_plan",
|
|
13289
13563
|
"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}.",
|
|
13290
13564
|
{
|
|
13291
|
-
plan:
|
|
13565
|
+
plan: import_zod55.z.record(import_zod55.z.unknown()).describe("The Build Plan object to validate.")
|
|
13292
13566
|
},
|
|
13293
13567
|
async ({ plan }) => validateBuildPlan(plan)
|
|
13294
13568
|
);
|
|
@@ -13296,12 +13570,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13296
13570
|
"apply_build_plan",
|
|
13297
13571
|
`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.`,
|
|
13298
13572
|
{
|
|
13299
|
-
plan:
|
|
13300
|
-
mode:
|
|
13301
|
-
locationId:
|
|
13302
|
-
metHandoffs:
|
|
13303
|
-
publishWorkflows:
|
|
13304
|
-
onConflict:
|
|
13573
|
+
plan: import_zod55.z.record(import_zod55.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
13574
|
+
mode: import_zod55.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
13575
|
+
locationId: import_zod55.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
|
|
13576
|
+
metHandoffs: import_zod55.z.array(import_zod55.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.'),
|
|
13577
|
+
publishWorkflows: import_zod55.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
|
|
13578
|
+
onConflict: import_zod55.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
|
|
13305
13579
|
},
|
|
13306
13580
|
async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
|
|
13307
13581
|
try {
|
|
@@ -13490,7 +13764,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13490
13764
|
resolved.set(q.key, rec);
|
|
13491
13765
|
}
|
|
13492
13766
|
if (!missing) break;
|
|
13493
|
-
if (attempt < 6) await
|
|
13767
|
+
if (attempt < 6) await sleep4(700 * attempt);
|
|
13494
13768
|
}
|
|
13495
13769
|
if (missing) {
|
|
13496
13770
|
throw new Error(
|
|
@@ -13504,9 +13778,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13504
13778
|
"install_intake_form",
|
|
13505
13779
|
"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.",
|
|
13506
13780
|
{
|
|
13507
|
-
dryRun:
|
|
13508
|
-
formId:
|
|
13509
|
-
formName:
|
|
13781
|
+
dryRun: import_zod55.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
13782
|
+
formId: import_zod55.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
13783
|
+
formName: import_zod55.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
|
|
13510
13784
|
},
|
|
13511
13785
|
async ({ dryRun, formId, formName }) => {
|
|
13512
13786
|
try {
|
|
@@ -13556,7 +13830,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13556
13830
|
break;
|
|
13557
13831
|
} catch (saveErr) {
|
|
13558
13832
|
if (justCreated && isFormNotYetPropagated(saveErr) && attempt < maxSaveAttempts) {
|
|
13559
|
-
await
|
|
13833
|
+
await sleep4(700 * attempt);
|
|
13560
13834
|
continue;
|
|
13561
13835
|
}
|
|
13562
13836
|
throw saveErr;
|
|
@@ -13568,7 +13842,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13568
13842
|
const verify = await formApiRequest(bc, "GET", `/${resolvedFormId}?locationId=${locationId2}`);
|
|
13569
13843
|
persistedCount = countFormFields(verify);
|
|
13570
13844
|
if (persistedCount > 0) break;
|
|
13571
|
-
if (attempt < 6) await
|
|
13845
|
+
if (attempt < 6) await sleep4(700 * attempt);
|
|
13572
13846
|
}
|
|
13573
13847
|
const fieldMap = {};
|
|
13574
13848
|
for (const [key, rec] of resolved) fieldMap[key] = rec.id;
|
|
@@ -13594,10 +13868,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13594
13868
|
"normalize_submission_to_brief",
|
|
13595
13869
|
'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).',
|
|
13596
13870
|
{
|
|
13597
|
-
formId:
|
|
13598
|
-
submissionId:
|
|
13599
|
-
fieldMap:
|
|
13600
|
-
preset:
|
|
13871
|
+
formId: import_zod55.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
13872
|
+
submissionId: import_zod55.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
13873
|
+
fieldMap: import_zod55.z.record(import_zod55.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
|
|
13874
|
+
preset: import_zod55.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
|
|
13601
13875
|
},
|
|
13602
13876
|
async ({ formId, submissionId, fieldMap, preset }) => {
|
|
13603
13877
|
try {
|
|
@@ -13626,7 +13900,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
13626
13900
|
const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
|
|
13627
13901
|
resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
|
|
13628
13902
|
}
|
|
13629
|
-
const presetSchema2 =
|
|
13903
|
+
const presetSchema2 = import_zod55.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
|
|
13630
13904
|
const presetParsed = presetSchema2.safeParse(preset);
|
|
13631
13905
|
const brief = normalizeSubmissionToBrief({
|
|
13632
13906
|
others,
|
|
@@ -13859,8 +14133,8 @@ function registerMetaTools(server2, installedVersion) {
|
|
|
13859
14133
|
|
|
13860
14134
|
// src/cli.ts
|
|
13861
14135
|
var import_node_util = require("node:util");
|
|
13862
|
-
var
|
|
13863
|
-
var
|
|
14136
|
+
var fs6 = __toESM(require("fs"));
|
|
14137
|
+
var path6 = __toESM(require("path"));
|
|
13864
14138
|
var import_crypto2 = require("crypto");
|
|
13865
14139
|
var EXIT_OK = 0;
|
|
13866
14140
|
var EXIT_USAGE = 2;
|
|
@@ -13899,9 +14173,9 @@ function errLine(msg2) {
|
|
|
13899
14173
|
function preflightWritable() {
|
|
13900
14174
|
try {
|
|
13901
14175
|
const dir = ensureAppDataDir();
|
|
13902
|
-
const probe =
|
|
13903
|
-
|
|
13904
|
-
|
|
14176
|
+
const probe = path6.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
|
|
14177
|
+
fs6.writeFileSync(probe, "ok");
|
|
14178
|
+
fs6.unlinkSync(probe);
|
|
13905
14179
|
return true;
|
|
13906
14180
|
} catch (error) {
|
|
13907
14181
|
errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -14151,7 +14425,7 @@ var bundledPkg = require_package();
|
|
|
14151
14425
|
var pkg = (() => {
|
|
14152
14426
|
try {
|
|
14153
14427
|
const onDisk = JSON.parse(
|
|
14154
|
-
|
|
14428
|
+
fs7.readFileSync(path7.resolve(__dirname, "..", "package.json"), "utf8")
|
|
14155
14429
|
);
|
|
14156
14430
|
if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
|
|
14157
14431
|
return { version: onDisk.version };
|
|
@@ -14163,7 +14437,7 @@ var pkg = (() => {
|
|
|
14163
14437
|
dotenv2.config();
|
|
14164
14438
|
{
|
|
14165
14439
|
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
14166
|
-
if (configDirOverride && !
|
|
14440
|
+
if (configDirOverride && !path7.isAbsolute(configDirOverride)) {
|
|
14167
14441
|
process.stderr.write(
|
|
14168
14442
|
`[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
|
|
14169
14443
|
`
|
|
@@ -14176,20 +14450,20 @@ process.on("unhandledRejection", (reason) => {
|
|
|
14176
14450
|
`);
|
|
14177
14451
|
});
|
|
14178
14452
|
function hardenSecretFilePerms() {
|
|
14179
|
-
const repoDir =
|
|
14453
|
+
const repoDir = path7.resolve(__dirname, "..");
|
|
14180
14454
|
const candidates = [
|
|
14181
|
-
{ file:
|
|
14455
|
+
{ file: path7.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
14182
14456
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
14183
|
-
{ file:
|
|
14457
|
+
{ file: path7.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
14184
14458
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
14185
14459
|
];
|
|
14186
14460
|
for (const { file, mode } of candidates) {
|
|
14187
14461
|
let current;
|
|
14188
14462
|
try {
|
|
14189
|
-
if (!
|
|
14190
|
-
current =
|
|
14463
|
+
if (!fs7.existsSync(file)) continue;
|
|
14464
|
+
current = fs7.statSync(file).mode & 511;
|
|
14191
14465
|
if (current !== mode) {
|
|
14192
|
-
|
|
14466
|
+
fs7.chmodSync(file, mode);
|
|
14193
14467
|
}
|
|
14194
14468
|
} catch (error) {
|
|
14195
14469
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -14363,6 +14637,7 @@ async function resolveAccessAndRegister() {
|
|
|
14363
14637
|
registerAllTools(server, client, registry, pkg.version);
|
|
14364
14638
|
registerEnableWorkflowBuilderTool(server);
|
|
14365
14639
|
registerFirebaseCaptureScriptTool(server);
|
|
14640
|
+
registerInteractiveCaptureTool(server);
|
|
14366
14641
|
if (fileCreds && !process.env.GHL_API_KEY) {
|
|
14367
14642
|
process.stderr.write(`[ghl-mcp] Loaded credentials from ${credentialsPath()}
|
|
14368
14643
|
`);
|