@elitedcs/ghl-mcp 3.32.0 → 3.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +27 -9
- package/dist/index.js +895 -310
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -31,9 +31,9 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "@elitedcs/ghl-mcp",
|
|
34
|
-
version: "3.
|
|
34
|
+
version: "3.34.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
36
|
+
description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
37
37
|
main: "dist/index.js",
|
|
38
38
|
bin: {
|
|
39
39
|
"ghl-mcp": "dist/index.js"
|
|
@@ -47,7 +47,7 @@ var require_package = __commonJS({
|
|
|
47
47
|
"CHANGELOG.md"
|
|
48
48
|
],
|
|
49
49
|
scripts: {
|
|
50
|
-
build: "esbuild src/index.ts --bundle --platform=node --target=
|
|
50
|
+
build: "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external",
|
|
51
51
|
setup: "node setup-wizard.mjs",
|
|
52
52
|
start: "node dist/index.js",
|
|
53
53
|
dev: "tsc --watch",
|
|
@@ -85,7 +85,7 @@ var require_package = __commonJS({
|
|
|
85
85
|
access: "public"
|
|
86
86
|
},
|
|
87
87
|
engines: {
|
|
88
|
-
node: ">=
|
|
88
|
+
node: ">=20"
|
|
89
89
|
},
|
|
90
90
|
dependencies: {
|
|
91
91
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -106,8 +106,8 @@ var require_package = __commonJS({
|
|
|
106
106
|
|
|
107
107
|
// src/index.ts
|
|
108
108
|
var dotenv2 = __toESM(require("dotenv"));
|
|
109
|
-
var
|
|
110
|
-
var
|
|
109
|
+
var path6 = __toESM(require("path"));
|
|
110
|
+
var fs6 = __toESM(require("fs"));
|
|
111
111
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
112
112
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
113
113
|
|
|
@@ -171,8 +171,8 @@ var GHLClient = class {
|
|
|
171
171
|
Version: version || GHL_API_VERSION
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
-
buildUrl(
|
|
175
|
-
const url = new URL(
|
|
174
|
+
buildUrl(path7, params) {
|
|
175
|
+
const url = new URL(path7, GHL_BASE_URL);
|
|
176
176
|
if (params) {
|
|
177
177
|
for (const [key, value] of Object.entries(params)) {
|
|
178
178
|
if (value !== void 0 && value !== null) {
|
|
@@ -182,8 +182,8 @@ var GHLClient = class {
|
|
|
182
182
|
}
|
|
183
183
|
return url.toString();
|
|
184
184
|
}
|
|
185
|
-
async request(method,
|
|
186
|
-
const url = this.buildUrl(
|
|
185
|
+
async request(method, path7, options = {}, attempt = 0) {
|
|
186
|
+
const url = this.buildUrl(path7, options.params);
|
|
187
187
|
const headers = this.buildHeaders(options.version);
|
|
188
188
|
const fetchOptions = {
|
|
189
189
|
method,
|
|
@@ -201,14 +201,14 @@ var GHLClient = class {
|
|
|
201
201
|
} catch (error) {
|
|
202
202
|
clearTimeout(timeout);
|
|
203
203
|
if (error instanceof Error && error.name === "AbortError") {
|
|
204
|
-
throw new Error(`Request timeout (30s): ${method} ${
|
|
204
|
+
throw new Error(`Request timeout (30s): ${method} ${path7}`);
|
|
205
205
|
}
|
|
206
206
|
if (!options.noRetry && attempt < MAX_RETRIES) {
|
|
207
207
|
const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
|
|
208
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
208
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path7}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
209
209
|
`);
|
|
210
210
|
await new Promise((r) => setTimeout(r, delay4));
|
|
211
|
-
return this.request(method,
|
|
211
|
+
return this.request(method, path7, options, attempt + 1);
|
|
212
212
|
}
|
|
213
213
|
throw error;
|
|
214
214
|
} finally {
|
|
@@ -216,10 +216,10 @@ var GHLClient = class {
|
|
|
216
216
|
}
|
|
217
217
|
if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
218
218
|
const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
|
|
219
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
219
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path7}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
220
220
|
`);
|
|
221
221
|
await new Promise((r) => setTimeout(r, delay4));
|
|
222
|
-
return this.request(method,
|
|
222
|
+
return this.request(method, path7, options, attempt + 1);
|
|
223
223
|
}
|
|
224
224
|
if (!response.ok) {
|
|
225
225
|
let errorBody = "";
|
|
@@ -228,7 +228,7 @@ var GHLClient = class {
|
|
|
228
228
|
} catch {
|
|
229
229
|
}
|
|
230
230
|
throw new Error(
|
|
231
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
231
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path7}
|
|
232
232
|
${errorBody}`
|
|
233
233
|
);
|
|
234
234
|
}
|
|
@@ -240,20 +240,20 @@ ${errorBody}`
|
|
|
240
240
|
return { message: text };
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
async get(
|
|
244
|
-
return this.request("GET",
|
|
243
|
+
async get(path7, options) {
|
|
244
|
+
return this.request("GET", path7, options);
|
|
245
245
|
}
|
|
246
|
-
async post(
|
|
247
|
-
return this.request("POST",
|
|
246
|
+
async post(path7, options) {
|
|
247
|
+
return this.request("POST", path7, options);
|
|
248
248
|
}
|
|
249
|
-
async put(
|
|
250
|
-
return this.request("PUT",
|
|
249
|
+
async put(path7, options) {
|
|
250
|
+
return this.request("PUT", path7, options);
|
|
251
251
|
}
|
|
252
|
-
async patch(
|
|
253
|
-
return this.request("PATCH",
|
|
252
|
+
async patch(path7, options) {
|
|
253
|
+
return this.request("PATCH", path7, options);
|
|
254
254
|
}
|
|
255
|
-
async delete(
|
|
256
|
-
return this.request("DELETE",
|
|
255
|
+
async delete(path7, options) {
|
|
256
|
+
return this.request("DELETE", path7, options);
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
259
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -262,7 +262,7 @@ ${errorBody}`
|
|
|
262
262
|
const id = providedId || this.defaultLocationId;
|
|
263
263
|
if (!id) {
|
|
264
264
|
throw new Error(
|
|
265
|
-
"locationId is required. Provide it as a parameter or set GHL_LOCATION_ID in your .env file."
|
|
265
|
+
"locationId is required. Provide it as a parameter, run switch_location to pick a registered sub-account (list_registered_locations shows what's registered), or set GHL_LOCATION_ID in your .env file."
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
return id;
|
|
@@ -300,6 +300,15 @@ var CredentialsSchema = import_zod.z.object({
|
|
|
300
300
|
signed_attestation: import_zod.z.string().optional()
|
|
301
301
|
});
|
|
302
302
|
function appDataDir() {
|
|
303
|
+
const override = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
304
|
+
if (override) {
|
|
305
|
+
if (!path.isAbsolute(override)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`GHL_MCP_CONFIG_DIR must be an absolute path (got "${override}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return override;
|
|
311
|
+
}
|
|
303
312
|
const home = os.homedir();
|
|
304
313
|
if (process.platform === "darwin") {
|
|
305
314
|
return path.join(home, "Library", "Application Support", APP_NAME);
|
|
@@ -406,6 +415,8 @@ var TokenRegistryDataSchema = import_zod2.z.object({
|
|
|
406
415
|
});
|
|
407
416
|
var TokenRegistry = class _TokenRegistry {
|
|
408
417
|
data;
|
|
418
|
+
loadFailure = null;
|
|
419
|
+
lastSaveError = null;
|
|
409
420
|
filePath;
|
|
410
421
|
constructor(filePath) {
|
|
411
422
|
if (filePath) {
|
|
@@ -454,18 +465,28 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
454
465
|
return TokenRegistryDataSchema.parse(JSON.parse(raw));
|
|
455
466
|
}
|
|
456
467
|
} catch (error) {
|
|
468
|
+
let backupNote = "backup also failed \u2014 the corrupted file is still at " + this.filePath;
|
|
457
469
|
try {
|
|
458
470
|
const backupPath = this.filePath + ".corrupted." + Date.now();
|
|
459
471
|
fs2.copyFileSync(this.filePath, backupPath);
|
|
472
|
+
backupNote = `backed up to ${backupPath}`;
|
|
460
473
|
process.stderr.write(`[ghl-mcp] ERROR: Token registry corrupted. Backed up to ${backupPath}
|
|
461
474
|
`);
|
|
462
475
|
} catch {
|
|
463
476
|
}
|
|
477
|
+
this.loadFailure = `Token registry could not be loaded (${backupNote}). All sub-account registrations are unavailable this session \u2014 re-register via register_location, or restore the backup over ${this.filePath} and restart.`;
|
|
464
478
|
process.stderr.write(`[ghl-mcp] Warning: Could not load token registry: ${error}
|
|
465
479
|
`);
|
|
466
480
|
}
|
|
467
481
|
return { tokens: {} };
|
|
468
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Non-null when the registry file existed but could not be parsed at
|
|
485
|
+
* startup (we fell back to an empty registry). Surfaced by health_check.
|
|
486
|
+
*/
|
|
487
|
+
getLoadFailure() {
|
|
488
|
+
return this.loadFailure;
|
|
489
|
+
}
|
|
469
490
|
save() {
|
|
470
491
|
const tmpPath = `${this.filePath}.tmp.${process.pid}.${(0, import_crypto.randomBytes)(8).toString("hex")}`;
|
|
471
492
|
try {
|
|
@@ -482,15 +503,25 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
482
503
|
fs2.chmodSync(this.filePath, 384);
|
|
483
504
|
} catch {
|
|
484
505
|
}
|
|
506
|
+
this.lastSaveError = null;
|
|
485
507
|
} catch (error) {
|
|
486
508
|
try {
|
|
487
509
|
fs2.unlinkSync(tmpPath);
|
|
488
510
|
} catch {
|
|
489
511
|
}
|
|
512
|
+
this.lastSaveError = error instanceof Error ? error.message : String(error);
|
|
490
513
|
process.stderr.write(`[ghl-mcp] Warning: Could not save token registry: ${error}
|
|
491
514
|
`);
|
|
492
515
|
}
|
|
493
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Non-null when the MOST RECENT save() failed (cleared by a later success).
|
|
519
|
+
* The seed CLI checks this after every write to turn the server's
|
|
520
|
+
* warn-and-continue policy into a hard exit code.
|
|
521
|
+
*/
|
|
522
|
+
getLastSaveError() {
|
|
523
|
+
return this.lastSaveError;
|
|
524
|
+
}
|
|
494
525
|
/**
|
|
495
526
|
* Get the API key for a specific location
|
|
496
527
|
*/
|
|
@@ -503,6 +534,13 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
503
534
|
getAgencyKey() {
|
|
504
535
|
return this.data.agencyKey;
|
|
505
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Store the agency/company-scoped API key (snapshots, agency-wide reads).
|
|
539
|
+
*/
|
|
540
|
+
setAgencyKey(key) {
|
|
541
|
+
this.data.agencyKey = key;
|
|
542
|
+
this.save();
|
|
543
|
+
}
|
|
506
544
|
/**
|
|
507
545
|
* Get Firebase config
|
|
508
546
|
*/
|
|
@@ -1412,7 +1450,7 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1412
1450
|
}
|
|
1413
1451
|
}
|
|
1414
1452
|
async performTokenRefresh() {
|
|
1415
|
-
const url = `${FIREBASE_TOKEN_URL}?key=${this.firebaseApiKey}`;
|
|
1453
|
+
const url = `${FIREBASE_TOKEN_URL}?key=${encodeURIComponent(this.firebaseApiKey)}`;
|
|
1416
1454
|
const body = `grant_type=refresh_token&refresh_token=${encodeURIComponent(this.refreshToken)}`;
|
|
1417
1455
|
const response = await fetch(url, {
|
|
1418
1456
|
method: "POST",
|
|
@@ -1924,6 +1962,48 @@ function escapeRegex(str) {
|
|
|
1924
1962
|
function errorMessage(error) {
|
|
1925
1963
|
return error instanceof Error ? error.message : String(error);
|
|
1926
1964
|
}
|
|
1965
|
+
function paginationInfo(opts) {
|
|
1966
|
+
const { returned, limit, nextHint } = opts;
|
|
1967
|
+
const total = typeof opts.total === "number" ? opts.total : void 0;
|
|
1968
|
+
if (total !== void 0) {
|
|
1969
|
+
const complete = returned >= total;
|
|
1970
|
+
return {
|
|
1971
|
+
returned,
|
|
1972
|
+
limit,
|
|
1973
|
+
total,
|
|
1974
|
+
complete,
|
|
1975
|
+
...complete ? {} : {
|
|
1976
|
+
note: `INCOMPLETE: showing ${returned} of ${total} total. ${nextHint ?? "Fetch further pages before treating this as the full list."}`
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
if (returned < limit) {
|
|
1981
|
+
return { returned, limit, complete: true };
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
returned,
|
|
1985
|
+
limit,
|
|
1986
|
+
complete: "unknown",
|
|
1987
|
+
note: `POSSIBLY INCOMPLETE: returned ${returned} which hit the limit of ${limit} \u2014 more may exist. ${nextHint ?? "Fetch further pages before treating this as the full list."}`
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function annotateListResponse(raw, listKey, limit, nextHint) {
|
|
1991
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
1992
|
+
const obj = raw;
|
|
1993
|
+
const list = obj[listKey];
|
|
1994
|
+
if (!Array.isArray(list)) return raw;
|
|
1995
|
+
let total;
|
|
1996
|
+
const meta = obj.meta;
|
|
1997
|
+
if (typeof meta === "object" && meta !== null) {
|
|
1998
|
+
const metaTotal = meta.total;
|
|
1999
|
+
if (typeof metaTotal === "number") total = metaTotal;
|
|
2000
|
+
}
|
|
2001
|
+
if (total === void 0 && typeof obj.total === "number") total = obj.total;
|
|
2002
|
+
return {
|
|
2003
|
+
...obj,
|
|
2004
|
+
_pagination: paginationInfo({ returned: list.length, limit, total, nextHint })
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
1927
2007
|
function safeTool(server2, name, description, schema, handler) {
|
|
1928
2008
|
server2.tool(name, description, schema, async (args) => {
|
|
1929
2009
|
try {
|
|
@@ -2017,7 +2097,16 @@ function registerContactTools(server2, client) {
|
|
|
2017
2097
|
order
|
|
2018
2098
|
}
|
|
2019
2099
|
});
|
|
2020
|
-
|
|
2100
|
+
const parsed = ContactSearchResponseSchema.parse(raw);
|
|
2101
|
+
return {
|
|
2102
|
+
...parsed,
|
|
2103
|
+
_pagination: paginationInfo({
|
|
2104
|
+
returned: parsed.contacts.length,
|
|
2105
|
+
limit: limit ?? 20,
|
|
2106
|
+
total: parsed.meta?.total,
|
|
2107
|
+
nextHint: "Pass startAfter + startAfterId from this response's meta to fetch the next page (both are required to advance)."
|
|
2108
|
+
})
|
|
2109
|
+
};
|
|
2021
2110
|
}
|
|
2022
2111
|
);
|
|
2023
2112
|
safeTool(
|
|
@@ -2308,7 +2397,7 @@ function registerConversationTools(server2, client) {
|
|
|
2308
2397
|
},
|
|
2309
2398
|
async ({ locationId: locationId2, contactId, assignedTo, query, status, limit, startAfterDate }) => {
|
|
2310
2399
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
2311
|
-
|
|
2400
|
+
const raw = await client.get("/conversations/search", {
|
|
2312
2401
|
params: {
|
|
2313
2402
|
locationId: resolvedLocationId,
|
|
2314
2403
|
contactId,
|
|
@@ -2319,6 +2408,12 @@ function registerConversationTools(server2, client) {
|
|
|
2319
2408
|
startAfterDate
|
|
2320
2409
|
}
|
|
2321
2410
|
});
|
|
2411
|
+
return annotateListResponse(
|
|
2412
|
+
raw,
|
|
2413
|
+
"conversations",
|
|
2414
|
+
limit ?? 20,
|
|
2415
|
+
"Pass startAfterDate from the last conversation's dateUpdated to fetch the next page."
|
|
2416
|
+
);
|
|
2322
2417
|
}
|
|
2323
2418
|
);
|
|
2324
2419
|
safeTool(
|
|
@@ -2463,7 +2558,7 @@ function registerOpportunityTools(server2, client) {
|
|
|
2463
2558
|
},
|
|
2464
2559
|
async (args) => {
|
|
2465
2560
|
const locationId2 = client.resolveLocationId(args.locationId);
|
|
2466
|
-
|
|
2561
|
+
const raw = await client.get("/opportunities/search", {
|
|
2467
2562
|
params: {
|
|
2468
2563
|
location_id: locationId2,
|
|
2469
2564
|
pipeline_id: args.pipelineId,
|
|
@@ -2480,6 +2575,12 @@ function registerOpportunityTools(server2, client) {
|
|
|
2480
2575
|
startDate: args.startDate
|
|
2481
2576
|
}
|
|
2482
2577
|
});
|
|
2578
|
+
return annotateListResponse(
|
|
2579
|
+
raw,
|
|
2580
|
+
"opportunities",
|
|
2581
|
+
args.limit ?? 20,
|
|
2582
|
+
"Pass startAfter + startAfterId from this response's meta to fetch the next page."
|
|
2583
|
+
);
|
|
2483
2584
|
}
|
|
2484
2585
|
);
|
|
2485
2586
|
safeTool(
|
|
@@ -3581,7 +3682,13 @@ function registerInvoiceTools(server2, client) {
|
|
|
3581
3682
|
if (startAt !== void 0) params.startAt = startAt;
|
|
3582
3683
|
if (endAt !== void 0) params.endAt = endAt;
|
|
3583
3684
|
if (search !== void 0) params.search = search;
|
|
3584
|
-
|
|
3685
|
+
const raw = await client.get("/invoices/", { params });
|
|
3686
|
+
return annotateListResponse(
|
|
3687
|
+
raw,
|
|
3688
|
+
"invoices",
|
|
3689
|
+
limit ?? 10,
|
|
3690
|
+
"Pass offset (current offset + limit) to fetch the next page."
|
|
3691
|
+
);
|
|
3585
3692
|
}
|
|
3586
3693
|
);
|
|
3587
3694
|
safeTool(
|
|
@@ -3617,8 +3724,8 @@ function registerInvoiceTools(server2, client) {
|
|
|
3617
3724
|
currency: import_zod17.z.string().describe("Currency code (e.g. USD)")
|
|
3618
3725
|
})).describe("Invoice line items"),
|
|
3619
3726
|
discount: import_zod17.z.object({
|
|
3620
|
-
type: import_zod17.z.string().optional(),
|
|
3621
|
-
value: import_zod17.z.number().optional()
|
|
3727
|
+
type: import_zod17.z.string().optional().describe("Discount type: 'percentage' or 'fixed'."),
|
|
3728
|
+
value: import_zod17.z.number().optional().describe("Discount amount: percent (0-100) when type=percentage, currency amount when type=fixed.")
|
|
3622
3729
|
}).optional().describe("Discount to apply"),
|
|
3623
3730
|
termsNotes: import_zod17.z.string().optional().describe("Terms and notes for the invoice"),
|
|
3624
3731
|
title: import_zod17.z.string().optional().describe("Invoice title")
|
|
@@ -3654,8 +3761,8 @@ function registerInvoiceTools(server2, client) {
|
|
|
3654
3761
|
currency: import_zod17.z.string().describe("Currency code (e.g. USD)")
|
|
3655
3762
|
})).optional().describe("Invoice line items"),
|
|
3656
3763
|
discount: import_zod17.z.object({
|
|
3657
|
-
type: import_zod17.z.string().optional(),
|
|
3658
|
-
value: import_zod17.z.number().optional()
|
|
3764
|
+
type: import_zod17.z.string().optional().describe("Discount type: 'percentage' or 'fixed'."),
|
|
3765
|
+
value: import_zod17.z.number().optional().describe("Discount amount: percent (0-100) when type=percentage, currency amount when type=fixed.")
|
|
3659
3766
|
}).optional().describe("Discount to apply"),
|
|
3660
3767
|
termsNotes: import_zod17.z.string().optional().describe("Terms and notes for the invoice"),
|
|
3661
3768
|
title: import_zod17.z.string().optional().describe("Invoice title")
|
|
@@ -4281,16 +4388,16 @@ function registerEmailTools(server2, client) {
|
|
|
4281
4388
|
function registerEmailBuilderInternalTools(server2, builderClient) {
|
|
4282
4389
|
const client = builderClient;
|
|
4283
4390
|
if (!client) return;
|
|
4284
|
-
async function builderRequest(method,
|
|
4391
|
+
async function builderRequest(method, path7, body) {
|
|
4285
4392
|
const headers = await client.buildHeaders();
|
|
4286
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
4393
|
+
const response = await fetch(`${EMAIL_BUILDER_BASE}${path7}`, {
|
|
4287
4394
|
method,
|
|
4288
4395
|
headers,
|
|
4289
4396
|
body: body ? JSON.stringify(body) : void 0
|
|
4290
4397
|
});
|
|
4291
4398
|
if (!response.ok) {
|
|
4292
4399
|
const text2 = await response.text();
|
|
4293
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
4400
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path7}
|
|
4294
4401
|
${text2}`);
|
|
4295
4402
|
}
|
|
4296
4403
|
const text = await response.text();
|
|
@@ -5212,7 +5319,10 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5212
5319
|
async ({ limit, skip }) => {
|
|
5213
5320
|
try {
|
|
5214
5321
|
const result = await client.listWorkflows(limit ?? 50, skip ?? 0);
|
|
5215
|
-
|
|
5322
|
+
const hint = "Pass skip (current skip + limit) to fetch the next page.";
|
|
5323
|
+
let annotated = annotateListResponse(result, "rows", limit ?? 50, hint);
|
|
5324
|
+
if (annotated === result) annotated = annotateListResponse(result, "workflows", limit ?? 50, hint);
|
|
5325
|
+
return jsonResponse(annotated);
|
|
5216
5326
|
} catch (error) {
|
|
5217
5327
|
return errorResponse(error);
|
|
5218
5328
|
}
|
|
@@ -5544,23 +5654,23 @@ var import_zod34 = require("zod");
|
|
|
5544
5654
|
function registerFunnelBuilderTools(server2, builderClient) {
|
|
5545
5655
|
const client = builderClient;
|
|
5546
5656
|
if (!client) return;
|
|
5547
|
-
async function internalGet(
|
|
5548
|
-
return client.request("GET",
|
|
5657
|
+
async function internalGet(path7) {
|
|
5658
|
+
return client.request("GET", path7);
|
|
5549
5659
|
}
|
|
5550
|
-
async function internalPost(
|
|
5551
|
-
return client.request("POST",
|
|
5660
|
+
async function internalPost(path7, body) {
|
|
5661
|
+
return client.request("POST", path7, body);
|
|
5552
5662
|
}
|
|
5553
|
-
async function internalPut(
|
|
5554
|
-
return client.request("PUT",
|
|
5663
|
+
async function internalPut(path7, body) {
|
|
5664
|
+
return client.request("PUT", path7, body);
|
|
5555
5665
|
}
|
|
5556
|
-
async function internalDelete(
|
|
5557
|
-
return client.request("DELETE",
|
|
5666
|
+
async function internalDelete(path7) {
|
|
5667
|
+
return client.request("DELETE", path7);
|
|
5558
5668
|
}
|
|
5559
|
-
async function funnelRequest(method,
|
|
5669
|
+
async function funnelRequest(method, path7, body) {
|
|
5560
5670
|
const headers = await client.buildHeaders();
|
|
5561
5671
|
headers.Origin = "https://app.gohighlevel.com";
|
|
5562
5672
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
5563
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
5673
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path7}`;
|
|
5564
5674
|
const options = { method, headers };
|
|
5565
5675
|
if (body && (method === "POST" || method === "PUT")) {
|
|
5566
5676
|
options.body = JSON.stringify(body);
|
|
@@ -5568,7 +5678,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
5568
5678
|
const response = await fetch(url, options);
|
|
5569
5679
|
if (!response.ok) {
|
|
5570
5680
|
const text2 = await response.text();
|
|
5571
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
5681
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path7}
|
|
5572
5682
|
${text2}`);
|
|
5573
5683
|
}
|
|
5574
5684
|
const text = await response.text();
|
|
@@ -5860,9 +5970,9 @@ function buildUpdateFormBody(name, formData) {
|
|
|
5860
5970
|
function registerFormBuilderTools(server2, builderClient) {
|
|
5861
5971
|
const client = builderClient;
|
|
5862
5972
|
if (!client) return;
|
|
5863
|
-
async function formRequest(method,
|
|
5973
|
+
async function formRequest(method, path7, body) {
|
|
5864
5974
|
const headers = await client.buildHeaders();
|
|
5865
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
5975
|
+
const url = `https://backend.leadconnectorhq.com/forms${path7}`;
|
|
5866
5976
|
const options = { method, headers };
|
|
5867
5977
|
if (body && (method === "POST" || method === "PUT")) {
|
|
5868
5978
|
options.body = JSON.stringify(body);
|
|
@@ -5870,7 +5980,7 @@ function registerFormBuilderTools(server2, builderClient) {
|
|
|
5870
5980
|
const response = await fetch(url, options);
|
|
5871
5981
|
if (!response.ok) {
|
|
5872
5982
|
const text2 = await response.text();
|
|
5873
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
5983
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path7}
|
|
5874
5984
|
${text2}`);
|
|
5875
5985
|
}
|
|
5876
5986
|
const text = await response.text();
|
|
@@ -5984,10 +6094,10 @@ ${text2}`);
|
|
|
5984
6094
|
},
|
|
5985
6095
|
async ({ formId, limit, skip }) => {
|
|
5986
6096
|
try {
|
|
5987
|
-
let
|
|
5988
|
-
if (formId)
|
|
5989
|
-
if (skip)
|
|
5990
|
-
const result = await formRequest("GET",
|
|
6097
|
+
let path7 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
6098
|
+
if (formId) path7 += `&formId=${formId}`;
|
|
6099
|
+
if (skip) path7 += `&skip=${skip}`;
|
|
6100
|
+
const result = await formRequest("GET", path7);
|
|
5991
6101
|
return {
|
|
5992
6102
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
5993
6103
|
};
|
|
@@ -6003,9 +6113,9 @@ var import_zod36 = require("zod");
|
|
|
6003
6113
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
6004
6114
|
const client = builderClient;
|
|
6005
6115
|
if (!client) return;
|
|
6006
|
-
async function pipelineRequest(method,
|
|
6116
|
+
async function pipelineRequest(method, path7, body) {
|
|
6007
6117
|
const headers = await client.buildHeaders();
|
|
6008
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
6118
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path7}`;
|
|
6009
6119
|
const options = { method, headers };
|
|
6010
6120
|
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
6011
6121
|
options.body = JSON.stringify(body);
|
|
@@ -6013,7 +6123,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
|
|
|
6013
6123
|
const response = await fetch(url, options);
|
|
6014
6124
|
if (!response.ok) {
|
|
6015
6125
|
const text2 = await response.text();
|
|
6016
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
6126
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path7}
|
|
6017
6127
|
${text2}`);
|
|
6018
6128
|
}
|
|
6019
6129
|
const text = await response.text();
|
|
@@ -6822,6 +6932,60 @@ The API key could not access location ${locationId2}. Make sure:
|
|
|
6822
6932
|
}
|
|
6823
6933
|
}
|
|
6824
6934
|
);
|
|
6935
|
+
server2.tool(
|
|
6936
|
+
"register_agency_key",
|
|
6937
|
+
"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.",
|
|
6938
|
+
{
|
|
6939
|
+
apiKey: import_zod38.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
|
|
6940
|
+
},
|
|
6941
|
+
async ({ apiKey: apiKey2 }) => {
|
|
6942
|
+
if (!registry2) {
|
|
6943
|
+
return {
|
|
6944
|
+
content: [{ type: "text", text: "Token registry not available. Check .ghl-tokens.json file." }],
|
|
6945
|
+
isError: true
|
|
6946
|
+
};
|
|
6947
|
+
}
|
|
6948
|
+
const testClient = new GHLClient({ apiKey: apiKey2 });
|
|
6949
|
+
try {
|
|
6950
|
+
const probe = await testClient.get("/locations/search", { params: { limit: 1, skip: 0 } });
|
|
6951
|
+
const locations = probe?.locations;
|
|
6952
|
+
if (!Array.isArray(locations)) {
|
|
6953
|
+
throw new Error(
|
|
6954
|
+
"the key was accepted by GHL but the response is not an agency location-search envelope (no locations array) \u2014 refusing to store it as the agency key"
|
|
6955
|
+
);
|
|
6956
|
+
}
|
|
6957
|
+
} catch (error) {
|
|
6958
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6959
|
+
return {
|
|
6960
|
+
content: [
|
|
6961
|
+
{
|
|
6962
|
+
type: "text",
|
|
6963
|
+
text: `Failed to validate the agency key: ${message}
|
|
6964
|
+
|
|
6965
|
+
Make sure:
|
|
6966
|
+
1. The Private Integration was created at the AGENCY level (Agency Settings > Private Integrations), not inside a sub-account
|
|
6967
|
+
2. The key is correct (it can only be copied once when created)
|
|
6968
|
+
3. The integration has the locations scope enabled`
|
|
6969
|
+
}
|
|
6970
|
+
],
|
|
6971
|
+
isError: true
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
const hadKey = Boolean(registry2.getAgencyKey());
|
|
6975
|
+
registry2.setAgencyKey(apiKey2);
|
|
6976
|
+
return {
|
|
6977
|
+
content: [
|
|
6978
|
+
{
|
|
6979
|
+
type: "text",
|
|
6980
|
+
text: `Agency key ${hadKey ? "replaced" : "registered"}: ${apiKey2.substring(0, 12)}...
|
|
6981
|
+
Saved to the token registry.
|
|
6982
|
+
|
|
6983
|
+
Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and list_available_locations (across all sub-accounts).`
|
|
6984
|
+
}
|
|
6985
|
+
]
|
|
6986
|
+
};
|
|
6987
|
+
}
|
|
6988
|
+
);
|
|
6825
6989
|
server2.tool(
|
|
6826
6990
|
"unregister_location",
|
|
6827
6991
|
"Remove a GHL sub-account from the token registry.",
|
|
@@ -7521,9 +7685,9 @@ var OBJECT_KEYS = ["contacts", "opportunity"];
|
|
|
7521
7685
|
function registerSmartListTools(server2, builderClient) {
|
|
7522
7686
|
const client = builderClient;
|
|
7523
7687
|
if (!client) return;
|
|
7524
|
-
async function smartListRequest(method,
|
|
7688
|
+
async function smartListRequest(method, path7, body) {
|
|
7525
7689
|
const headers = await client.buildHeaders();
|
|
7526
|
-
const url = `${SMARTLIST_BASE}${
|
|
7690
|
+
const url = `${SMARTLIST_BASE}${path7}`;
|
|
7527
7691
|
const options = { method, headers };
|
|
7528
7692
|
if (body && (method === "POST" || method === "PUT")) {
|
|
7529
7693
|
options.body = JSON.stringify(body);
|
|
@@ -7531,7 +7695,7 @@ function registerSmartListTools(server2, builderClient) {
|
|
|
7531
7695
|
const response = await fetch(url, options);
|
|
7532
7696
|
if (!response.ok) {
|
|
7533
7697
|
const text2 = await response.text();
|
|
7534
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
7698
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path7}
|
|
7535
7699
|
${text2}`);
|
|
7536
7700
|
}
|
|
7537
7701
|
const text = await response.text();
|
|
@@ -7663,12 +7827,12 @@ var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
|
|
|
7663
7827
|
function registerReputationTools(server2, builderClient) {
|
|
7664
7828
|
const client = builderClient;
|
|
7665
7829
|
if (!client) return;
|
|
7666
|
-
async function reputationRequest(method,
|
|
7830
|
+
async function reputationRequest(method, path7) {
|
|
7667
7831
|
const headers = await client.buildHeaders();
|
|
7668
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
7832
|
+
const response = await fetch(`${REPUTATION_BASE}${path7}`, { method, headers });
|
|
7669
7833
|
if (!response.ok) {
|
|
7670
7834
|
const text2 = await response.text();
|
|
7671
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
7835
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path7}
|
|
7672
7836
|
${text2}`);
|
|
7673
7837
|
}
|
|
7674
7838
|
const text = await response.text();
|
|
@@ -7783,16 +7947,16 @@ var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
|
|
|
7783
7947
|
function registerMembershipTools(server2, builderClient) {
|
|
7784
7948
|
const client = builderClient;
|
|
7785
7949
|
if (!client) return;
|
|
7786
|
-
async function membershipRequest(
|
|
7950
|
+
async function membershipRequest(path7, method = "GET", body) {
|
|
7787
7951
|
const headers = await client.buildHeaders();
|
|
7788
|
-
const response = await fetch(`${MEMBERSHIP_BASE}${
|
|
7952
|
+
const response = await fetch(`${MEMBERSHIP_BASE}${path7}`, {
|
|
7789
7953
|
method,
|
|
7790
7954
|
headers,
|
|
7791
7955
|
body: body ? JSON.stringify(body) : void 0
|
|
7792
7956
|
});
|
|
7793
7957
|
if (!response.ok) {
|
|
7794
7958
|
const text2 = await response.text();
|
|
7795
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
7959
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path7}
|
|
7796
7960
|
${text2}`);
|
|
7797
7961
|
}
|
|
7798
7962
|
const text = await response.text();
|
|
@@ -8345,6 +8509,11 @@ ${errors.join("\n")}` : "\nNo errors!",
|
|
|
8345
8509
|
|
|
8346
8510
|
// src/tools/validators.ts
|
|
8347
8511
|
var import_zod47 = require("zod");
|
|
8512
|
+
var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
|
|
8513
|
+
var ID_SHAPE = /^[A-Za-z0-9]{17,}$/;
|
|
8514
|
+
function isIdShaped(s) {
|
|
8515
|
+
return typeof s === "string" && ID_SHAPE.test(s);
|
|
8516
|
+
}
|
|
8348
8517
|
function extractFromTrigger(trigger, refs) {
|
|
8349
8518
|
const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
|
|
8350
8519
|
const where = `trigger "${triggerName}"`;
|
|
@@ -8354,10 +8523,21 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8354
8523
|
const field = typeof c.field === "string" ? c.field : null;
|
|
8355
8524
|
const value = c.value;
|
|
8356
8525
|
if (!field || value === void 0 || value === null) continue;
|
|
8526
|
+
const childWhere = `${where} \u2192 conditions[${i}] (field=${field})`;
|
|
8527
|
+
if (field.startsWith("contact.")) {
|
|
8528
|
+
const suffix = field.slice("contact.".length);
|
|
8529
|
+
if (suffix === "assignedTo") {
|
|
8530
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
8531
|
+
if (isIdShaped(v)) refs.push({ kind: "user", id: v, where: childWhere });
|
|
8532
|
+
}
|
|
8533
|
+
} else if (isIdShaped(suffix)) {
|
|
8534
|
+
refs.push({ kind: "custom_field", id: suffix, where: childWhere });
|
|
8535
|
+
}
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8357
8538
|
const valueAsArray = Array.isArray(value) ? value : [value];
|
|
8358
8539
|
for (const v of valueAsArray) {
|
|
8359
8540
|
if (typeof v !== "string") continue;
|
|
8360
|
-
const childWhere = `${where} \u2192 conditions[${i}] (field=${field})`;
|
|
8361
8541
|
switch (field) {
|
|
8362
8542
|
case "opportunity.pipelineId":
|
|
8363
8543
|
refs.push({ kind: "pipeline", id: v, where: childWhere });
|
|
@@ -8366,7 +8546,6 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8366
8546
|
refs.push({ kind: "stage", id: v, where: childWhere });
|
|
8367
8547
|
break;
|
|
8368
8548
|
case "opportunity.assignedTo":
|
|
8369
|
-
case "contact.assignedTo":
|
|
8370
8549
|
refs.push({ kind: "user", id: v, where: childWhere });
|
|
8371
8550
|
break;
|
|
8372
8551
|
case "workflow.id":
|
|
@@ -8384,17 +8563,18 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8384
8563
|
}
|
|
8385
8564
|
}
|
|
8386
8565
|
}
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8566
|
+
}
|
|
8567
|
+
function customInputId(attr, filterFields) {
|
|
8568
|
+
const arr = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : [];
|
|
8569
|
+
for (const raw of arr) {
|
|
8570
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
8571
|
+
const c = raw;
|
|
8572
|
+
if (typeof c.filterField === "string" && filterFields.includes(c.filterField)) {
|
|
8573
|
+
if (isIdShaped(c.secondValue)) return c.secondValue;
|
|
8574
|
+
if (isIdShaped(c.value)) return c.value;
|
|
8396
8575
|
}
|
|
8397
8576
|
}
|
|
8577
|
+
return void 0;
|
|
8398
8578
|
}
|
|
8399
8579
|
function extractFromAction(action, refs) {
|
|
8400
8580
|
const type = typeof action.type === "string" ? action.type : "unknown";
|
|
@@ -8402,258 +8582,267 @@ function extractFromAction(action, refs) {
|
|
|
8402
8582
|
const where = `action "${name}" (${type})`;
|
|
8403
8583
|
const attr = action.attributes ?? {};
|
|
8404
8584
|
switch (type) {
|
|
8405
|
-
|
|
8585
|
+
// ── Contact custom fields (only id-shaped values; standard names skipped) ──
|
|
8586
|
+
case "update_contact_field":
|
|
8587
|
+
case "create_update_contact": {
|
|
8406
8588
|
const fields = Array.isArray(attr.fields) ? attr.fields : [];
|
|
8407
8589
|
for (let i = 0; i < fields.length; i++) {
|
|
8408
8590
|
const f = fields[i];
|
|
8409
|
-
if (
|
|
8410
|
-
refs.push({
|
|
8411
|
-
kind: "custom_field",
|
|
8412
|
-
id: f.field,
|
|
8413
|
-
where: `${where} \u2192 fields[${i}]`
|
|
8414
|
-
});
|
|
8415
|
-
}
|
|
8591
|
+
if (isIdShaped(f?.field)) refs.push({ kind: "custom_field", id: f.field, where: `${where} \u2192 fields[${i}]` });
|
|
8416
8592
|
}
|
|
8417
8593
|
break;
|
|
8418
8594
|
}
|
|
8595
|
+
// ── User refs ──
|
|
8419
8596
|
case "internal_notification": {
|
|
8420
8597
|
const notif = attr.notification ?? {};
|
|
8421
|
-
if (
|
|
8422
|
-
refs.push({
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
});
|
|
8598
|
+
if (isIdShaped(notif.selectedUser))
|
|
8599
|
+
refs.push({ kind: "user", id: notif.selectedUser, where: `${where} \u2192 notification.selectedUser` });
|
|
8600
|
+
const sms = attr.sms ?? {};
|
|
8601
|
+
const smsUsers = Array.isArray(sms.selectedUser) ? sms.selectedUser : [];
|
|
8602
|
+
for (let i = 0; i < smsUsers.length; i++) {
|
|
8603
|
+
if (isIdShaped(smsUsers[i])) refs.push({ kind: "user", id: smsUsers[i], where: `${where} \u2192 sms.selectedUser[${i}]` });
|
|
8427
8604
|
}
|
|
8428
8605
|
break;
|
|
8429
8606
|
}
|
|
8430
|
-
case "task_notification":
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
id: attr.assignedTo,
|
|
8435
|
-
where: `${where} \u2192 assignedTo`
|
|
8436
|
-
});
|
|
8437
|
-
}
|
|
8607
|
+
case "task_notification":
|
|
8608
|
+
case "task-notification": {
|
|
8609
|
+
if (isIdShaped(attr.assignedTo))
|
|
8610
|
+
refs.push({ kind: "user", id: attr.assignedTo, where: `${where} \u2192 assignedTo` });
|
|
8438
8611
|
break;
|
|
8439
8612
|
}
|
|
8613
|
+
// ── Workflow refs ──
|
|
8440
8614
|
case "remove_from_workflow": {
|
|
8441
|
-
if (typeof attr.workflowId === "string") {
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
});
|
|
8457
|
-
}
|
|
8458
|
-
}
|
|
8615
|
+
if (typeof attr.workflowId === "string") refs.push({ kind: "workflow", id: attr.workflowId, where: `${where} \u2192 workflowId` });
|
|
8616
|
+
const arr = Array.isArray(attr.workflow_id) ? attr.workflow_id : [];
|
|
8617
|
+
for (let i = 0; i < arr.length; i++) if (typeof arr[i] === "string") refs.push({ kind: "workflow", id: arr[i], where: `${where} \u2192 workflow_id[${i}]` });
|
|
8618
|
+
break;
|
|
8619
|
+
}
|
|
8620
|
+
// ── Opportunity refs — FOUR distinct shapes ──
|
|
8621
|
+
case "internal_create_opportunity": {
|
|
8622
|
+
if (isIdShaped(attr.pipelineId)) refs.push({ kind: "pipeline", id: attr.pipelineId, where: `${where} \u2192 pipelineId` });
|
|
8623
|
+
const stage = customInputId(attr, ["pipelineStageId"]);
|
|
8624
|
+
if (stage) refs.push({ kind: "stage", id: stage, where: `${where} \u2192 __customInputFields__.pipelineStageId` });
|
|
8625
|
+
break;
|
|
8626
|
+
}
|
|
8627
|
+
case "create_opportunity": {
|
|
8628
|
+
if (isIdShaped(attr.pipeline_id)) refs.push({ kind: "pipeline", id: attr.pipeline_id, where: `${where} \u2192 pipeline_id` });
|
|
8629
|
+
if (isIdShaped(attr.pipeline_stage_id)) refs.push({ kind: "stage", id: attr.pipeline_stage_id, where: `${where} \u2192 pipeline_stage_id` });
|
|
8459
8630
|
break;
|
|
8460
8631
|
}
|
|
8461
8632
|
case "internal_update_opportunity": {
|
|
8462
|
-
const
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8633
|
+
const pipe = customInputId(attr, ["pipelineId"]);
|
|
8634
|
+
if (pipe) refs.push({ kind: "pipeline", id: pipe, where: `${where} \u2192 __customInputFields__.pipelineId` });
|
|
8635
|
+
const stage = customInputId(attr, ["pipelineStageId"]);
|
|
8636
|
+
if (stage) refs.push({ kind: "stage", id: stage, where: `${where} \u2192 __customInputFields__.pipelineStageId` });
|
|
8637
|
+
break;
|
|
8638
|
+
}
|
|
8639
|
+
case "find_opportunity": {
|
|
8640
|
+
const pipe = customInputId(attr, ["pipeline_id", "pipelineId"]);
|
|
8641
|
+
if (pipe) refs.push({ kind: "pipeline", id: pipe, where: `${where} \u2192 __customInputFields__.pipeline_id` });
|
|
8642
|
+
break;
|
|
8643
|
+
}
|
|
8644
|
+
// ── if_else condition node: custom-field id is conditionSubType (NOT conditionValue) ──
|
|
8645
|
+
case "if_else": {
|
|
8646
|
+
const branches = Array.isArray(attr.branches) ? attr.branches : [];
|
|
8647
|
+
for (const b of branches) {
|
|
8648
|
+
const segs = b && typeof b === "object" && Array.isArray(b.segments) ? b.segments : [];
|
|
8649
|
+
for (const s of segs) {
|
|
8650
|
+
const conds = s && typeof s === "object" && Array.isArray(s.conditions) ? s.conditions : [];
|
|
8651
|
+
for (const raw of conds) {
|
|
8652
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
8653
|
+
const c = raw;
|
|
8654
|
+
if (c.conditionType === "contact_detail" && c.conditionSubType !== "tags" && isIdShaped(c.conditionSubType)) {
|
|
8655
|
+
refs.push({ kind: "custom_field", id: c.conditionSubType, where: `${where} \u2192 if_else condition (custom field)` });
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8470
8658
|
}
|
|
8471
8659
|
}
|
|
8472
8660
|
break;
|
|
8473
8661
|
}
|
|
8474
8662
|
}
|
|
8475
8663
|
}
|
|
8664
|
+
function collectIds(envelope, listKeys) {
|
|
8665
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8666
|
+
let arr = null;
|
|
8667
|
+
if (Array.isArray(envelope)) arr = envelope;
|
|
8668
|
+
else if (envelope && typeof envelope === "object") {
|
|
8669
|
+
const e = envelope;
|
|
8670
|
+
for (const k of listKeys) if (Array.isArray(e[k])) {
|
|
8671
|
+
arr = e[k];
|
|
8672
|
+
break;
|
|
8673
|
+
}
|
|
8674
|
+
}
|
|
8675
|
+
if (arr) for (const item of arr) {
|
|
8676
|
+
if (typeof item === "object" && item !== null) {
|
|
8677
|
+
const o = item;
|
|
8678
|
+
const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : null;
|
|
8679
|
+
if (id) ids.add(id);
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
return { ids, found: arr !== null };
|
|
8683
|
+
}
|
|
8684
|
+
async function fetchAndBuildLookups(client, builderClient, locationId2, workflowExistence) {
|
|
8685
|
+
const fetches = {
|
|
8686
|
+
pipelines: client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }),
|
|
8687
|
+
customFields: client.get(`/locations/${locationId2}/customFields`),
|
|
8688
|
+
users: client.get("/users/", { params: { locationId: locationId2 } }),
|
|
8689
|
+
forms: client.get("/forms/", { params: { locationId: locationId2 } }),
|
|
8690
|
+
calendars: client.get("/calendars/", { params: { locationId: locationId2 } }),
|
|
8691
|
+
surveys: client.get("/surveys/", { params: { locationId: locationId2 } })
|
|
8692
|
+
};
|
|
8693
|
+
const keys = Object.keys(fetches);
|
|
8694
|
+
const settled = await Promise.allSettled(Object.values(fetches));
|
|
8695
|
+
const data = {};
|
|
8696
|
+
const failed = /* @__PURE__ */ new Set();
|
|
8697
|
+
for (let i = 0; i < keys.length; i++) {
|
|
8698
|
+
if (settled[i].status === "fulfilled") data[keys[i]] = settled[i].value;
|
|
8699
|
+
else {
|
|
8700
|
+
data[keys[i]] = null;
|
|
8701
|
+
failed.add(keys[i]);
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
const status = {
|
|
8705
|
+
pipeline: "loaded",
|
|
8706
|
+
stage: "loaded",
|
|
8707
|
+
custom_field: "loaded",
|
|
8708
|
+
user: "loaded",
|
|
8709
|
+
workflow: "loaded",
|
|
8710
|
+
form: "loaded",
|
|
8711
|
+
calendar: "loaded",
|
|
8712
|
+
survey: "loaded"
|
|
8713
|
+
};
|
|
8714
|
+
const pipelines = /* @__PURE__ */ new Set();
|
|
8715
|
+
const stages = /* @__PURE__ */ new Set();
|
|
8716
|
+
if (failed.has("pipelines")) {
|
|
8717
|
+
status.pipeline = "failed";
|
|
8718
|
+
status.stage = "failed";
|
|
8719
|
+
} else {
|
|
8720
|
+
try {
|
|
8721
|
+
const parsed = PipelinesResponseSchema.parse(data.pipelines);
|
|
8722
|
+
for (const p of parsed.pipelines) {
|
|
8723
|
+
pipelines.add(p.id);
|
|
8724
|
+
for (const s of p.stages) stages.add(s.id);
|
|
8725
|
+
}
|
|
8726
|
+
} catch {
|
|
8727
|
+
status.pipeline = "unparseable";
|
|
8728
|
+
status.stage = "unparseable";
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
function setFrom(key, cat, listKeys) {
|
|
8732
|
+
if (failed.has(key)) {
|
|
8733
|
+
status[cat] = "failed";
|
|
8734
|
+
return /* @__PURE__ */ new Set();
|
|
8735
|
+
}
|
|
8736
|
+
const { ids, found } = collectIds(data[key], listKeys);
|
|
8737
|
+
if (!found) status[cat] = "unparseable";
|
|
8738
|
+
return ids;
|
|
8739
|
+
}
|
|
8740
|
+
const custom_field = setFrom("customFields", "custom_field", ["customFields"]);
|
|
8741
|
+
const user = setFrom("users", "user", ["users"]);
|
|
8742
|
+
const form = setFrom("forms", "form", ["forms"]);
|
|
8743
|
+
const calendar = setFrom("calendars", "calendar", ["calendars"]);
|
|
8744
|
+
const survey = setFrom("surveys", "survey", ["surveys"]);
|
|
8745
|
+
status.workflow = workflowExistence.complete ? "loaded" : "incomplete";
|
|
8746
|
+
return { pipelines, stages, custom_field, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8747
|
+
}
|
|
8748
|
+
function auditOneWorkflow(workflow, selfId, lookups) {
|
|
8749
|
+
const refs = [];
|
|
8750
|
+
const triggers = Array.isArray(workflow.triggers) ? workflow.triggers : [];
|
|
8751
|
+
for (const t of triggers) extractFromTrigger(t, refs);
|
|
8752
|
+
const actions = Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : [];
|
|
8753
|
+
for (const a of actions) extractFromAction(a, refs);
|
|
8754
|
+
return refs;
|
|
8755
|
+
}
|
|
8756
|
+
function setForCategory(lookups, kind) {
|
|
8757
|
+
switch (kind) {
|
|
8758
|
+
case "pipeline":
|
|
8759
|
+
return lookups.pipelines;
|
|
8760
|
+
case "stage":
|
|
8761
|
+
return lookups.stages;
|
|
8762
|
+
default:
|
|
8763
|
+
return lookups[kind];
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
function checkRefs(refs, selfId, lookups) {
|
|
8767
|
+
const findings = [];
|
|
8768
|
+
for (const ref of refs) {
|
|
8769
|
+
const st = lookups.status[ref.kind];
|
|
8770
|
+
if (st !== "loaded") {
|
|
8771
|
+
findings.push({
|
|
8772
|
+
severity: "unverified",
|
|
8773
|
+
category: ref.kind,
|
|
8774
|
+
id: ref.id,
|
|
8775
|
+
where: ref.where,
|
|
8776
|
+
message: `${ref.kind} reference could not be verified \u2014 the ${ref.kind} list ${st === "incomplete" ? "is too large to fully load" : "failed to load or was unreadable"}. Re-run; not reported as broken.`
|
|
8777
|
+
});
|
|
8778
|
+
continue;
|
|
8779
|
+
}
|
|
8780
|
+
const valid = setForCategory(lookups, ref.kind).has(ref.id);
|
|
8781
|
+
if (valid) {
|
|
8782
|
+
if (ref.kind === "workflow" && ref.id === selfId)
|
|
8783
|
+
findings.push({ severity: "warning", category: ref.kind, id: ref.id, where: ref.where, message: `${ref.kind} id "${ref.id}" is valid (self-reference).` });
|
|
8784
|
+
} else {
|
|
8785
|
+
findings.push({
|
|
8786
|
+
severity: "error",
|
|
8787
|
+
category: ref.kind,
|
|
8788
|
+
id: ref.id,
|
|
8789
|
+
where: ref.where,
|
|
8790
|
+
message: `${ref.kind} id "${ref.id}" does not exist in this location \u2014 GHL will silently skip this action and all subsequent actions when the workflow runs.`
|
|
8791
|
+
});
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
return findings;
|
|
8795
|
+
}
|
|
8796
|
+
async function fullWorkflowCatalog(builderClient) {
|
|
8797
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8798
|
+
const rows = [];
|
|
8799
|
+
const limit = 100;
|
|
8800
|
+
let skip = 0;
|
|
8801
|
+
let complete = true;
|
|
8802
|
+
for (let page = 0; page < 50; page++) {
|
|
8803
|
+
const resp = await builderClient.listWorkflows(limit, skip);
|
|
8804
|
+
const found = Array.isArray(resp?.rows) || Array.isArray(resp?.workflows) || Array.isArray(resp);
|
|
8805
|
+
if (!found) {
|
|
8806
|
+
if (page === 0) throw new Error("Could not read the workflow list (unexpected response shape) \u2014 try again.");
|
|
8807
|
+
complete = false;
|
|
8808
|
+
break;
|
|
8809
|
+
}
|
|
8810
|
+
const arr = Array.isArray(resp?.rows) ? resp.rows : Array.isArray(resp?.workflows) ? resp.workflows : Array.isArray(resp) ? resp : [];
|
|
8811
|
+
for (const w of arr) {
|
|
8812
|
+
if (w && typeof w === "object") {
|
|
8813
|
+
const o = w;
|
|
8814
|
+
const id = typeof o._id === "string" ? o._id : typeof o.id === "string" ? o.id : null;
|
|
8815
|
+
if (id) {
|
|
8816
|
+
ids.add(id);
|
|
8817
|
+
rows.push({ id, name: typeof o.name === "string" ? o.name : id });
|
|
8818
|
+
}
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
if (arr.length < limit) break;
|
|
8822
|
+
skip += limit;
|
|
8823
|
+
if (page === 49) complete = false;
|
|
8824
|
+
}
|
|
8825
|
+
return { ids, rows, complete };
|
|
8826
|
+
}
|
|
8476
8827
|
function registerValidatorTools(server2, client, builderClient) {
|
|
8477
8828
|
if (!builderClient) return;
|
|
8478
8829
|
server2.tool(
|
|
8479
8830
|
"validate_workflow",
|
|
8480
|
-
"Pre-flight ID validation for
|
|
8481
|
-
{
|
|
8482
|
-
workflowId: import_zod47.z.string().describe("The workflow ID to validate.")
|
|
8483
|
-
},
|
|
8831
|
+
"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'.",
|
|
8832
|
+
{ workflowId: import_zod47.z.string().describe("The workflow ID to validate.") },
|
|
8484
8833
|
async ({ workflowId }) => {
|
|
8485
8834
|
try {
|
|
8486
|
-
let collectIds2 = function(envelope, listKey) {
|
|
8487
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8488
|
-
if (envelope && typeof envelope === "object") {
|
|
8489
|
-
const e = envelope;
|
|
8490
|
-
const arr = Array.isArray(e[listKey]) ? e[listKey] : Array.isArray(envelope) ? envelope : [];
|
|
8491
|
-
for (const item of arr) {
|
|
8492
|
-
if (typeof item === "object" && item !== null) {
|
|
8493
|
-
const o = item;
|
|
8494
|
-
const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : null;
|
|
8495
|
-
if (id) ids.add(id);
|
|
8496
|
-
}
|
|
8497
|
-
}
|
|
8498
|
-
}
|
|
8499
|
-
return ids;
|
|
8500
|
-
};
|
|
8501
|
-
var collectIds = collectIds2;
|
|
8502
8835
|
const workflow = await builderClient.getWorkflow(workflowId);
|
|
8503
8836
|
if (!workflow) return errorResponse(new Error(`Workflow ${workflowId} not found`));
|
|
8504
8837
|
const refs = [];
|
|
8505
|
-
const
|
|
8506
|
-
for (const
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
const
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
if (refs.length === 0) {
|
|
8514
|
-
const empty = {
|
|
8515
|
-
workflowId,
|
|
8516
|
-
workflowName: workflow.name,
|
|
8517
|
-
status: "ok",
|
|
8518
|
-
references_scanned: 0,
|
|
8519
|
-
issues_count: 0,
|
|
8520
|
-
findings: []
|
|
8521
|
-
};
|
|
8522
|
-
return jsonResponse(empty);
|
|
8523
|
-
}
|
|
8524
|
-
const refCategories = new Set(refs.map((r) => r.kind));
|
|
8525
|
-
const locationId2 = client.defaultLocationId;
|
|
8526
|
-
const fetches = {};
|
|
8527
|
-
if (refCategories.has("pipeline") || refCategories.has("stage")) {
|
|
8528
|
-
fetches.pipelines = client.get("/opportunities/pipelines", { params: { locationId: locationId2 } });
|
|
8529
|
-
}
|
|
8530
|
-
if (refCategories.has("custom_field")) {
|
|
8531
|
-
fetches.customFields = client.get(`/locations/${locationId2}/customFields`);
|
|
8532
|
-
}
|
|
8533
|
-
if (refCategories.has("user")) {
|
|
8534
|
-
fetches.users = client.get("/users/", { params: { locationId: locationId2 } });
|
|
8535
|
-
}
|
|
8536
|
-
if (refCategories.has("workflow")) {
|
|
8537
|
-
fetches.workflows = builderClient.listWorkflows(200);
|
|
8538
|
-
}
|
|
8539
|
-
if (refCategories.has("form")) {
|
|
8540
|
-
fetches.forms = client.get("/forms/", { params: { locationId: locationId2 } });
|
|
8541
|
-
}
|
|
8542
|
-
if (refCategories.has("calendar")) {
|
|
8543
|
-
fetches.calendars = client.get("/calendars/", { params: { locationId: locationId2 } });
|
|
8544
|
-
}
|
|
8545
|
-
if (refCategories.has("survey")) {
|
|
8546
|
-
fetches.surveys = client.get("/surveys/", { params: { locationId: locationId2 } });
|
|
8547
|
-
}
|
|
8548
|
-
const results = await Promise.allSettled(Object.values(fetches));
|
|
8549
|
-
const keys = Object.keys(fetches);
|
|
8550
|
-
const data = {};
|
|
8551
|
-
for (let i = 0; i < keys.length; i++) {
|
|
8552
|
-
const r = results[i];
|
|
8553
|
-
data[keys[i]] = r.status === "fulfilled" ? r.value : null;
|
|
8554
|
-
}
|
|
8555
|
-
const validPipelineIds = /* @__PURE__ */ new Set();
|
|
8556
|
-
const validStageIds = /* @__PURE__ */ new Set();
|
|
8557
|
-
const stageToPipeline = /* @__PURE__ */ new Map();
|
|
8558
|
-
if (data.pipelines) {
|
|
8559
|
-
try {
|
|
8560
|
-
const parsed = PipelinesResponseSchema.parse(data.pipelines);
|
|
8561
|
-
for (const p of parsed.pipelines) {
|
|
8562
|
-
validPipelineIds.add(p.id);
|
|
8563
|
-
for (const s of p.stages) {
|
|
8564
|
-
validStageIds.add(s.id);
|
|
8565
|
-
stageToPipeline.set(s.id, p.id);
|
|
8566
|
-
}
|
|
8567
|
-
}
|
|
8568
|
-
} catch {
|
|
8569
|
-
}
|
|
8570
|
-
}
|
|
8571
|
-
const validCustomFieldIds = /* @__PURE__ */ new Set();
|
|
8572
|
-
if (data.customFields && typeof data.customFields === "object") {
|
|
8573
|
-
const cf = data.customFields;
|
|
8574
|
-
const arr = Array.isArray(cf.customFields) ? cf.customFields : Array.isArray(cf) ? cf : [];
|
|
8575
|
-
for (const f of arr) {
|
|
8576
|
-
if (typeof f === "object" && f !== null && typeof f.id === "string") {
|
|
8577
|
-
validCustomFieldIds.add(f.id);
|
|
8578
|
-
}
|
|
8579
|
-
}
|
|
8580
|
-
}
|
|
8581
|
-
const validUserIds = /* @__PURE__ */ new Set();
|
|
8582
|
-
if (data.users && typeof data.users === "object") {
|
|
8583
|
-
const u = data.users;
|
|
8584
|
-
const arr = Array.isArray(u.users) ? u.users : Array.isArray(u) ? u : [];
|
|
8585
|
-
for (const user of arr) {
|
|
8586
|
-
if (typeof user === "object" && user !== null && typeof user.id === "string") {
|
|
8587
|
-
validUserIds.add(user.id);
|
|
8588
|
-
}
|
|
8589
|
-
}
|
|
8590
|
-
}
|
|
8591
|
-
const validWorkflowIds = /* @__PURE__ */ new Set();
|
|
8592
|
-
if (data.workflows && typeof data.workflows === "object") {
|
|
8593
|
-
const w = data.workflows;
|
|
8594
|
-
const arr = Array.isArray(w.rows) ? w.rows : Array.isArray(w.workflows) ? w.workflows : Array.isArray(w) ? w : [];
|
|
8595
|
-
for (const wf of arr) {
|
|
8596
|
-
if (typeof wf === "object" && wf !== null) {
|
|
8597
|
-
const o = wf;
|
|
8598
|
-
const id = typeof o._id === "string" ? o._id : typeof o.id === "string" ? o.id : null;
|
|
8599
|
-
if (id) validWorkflowIds.add(id);
|
|
8600
|
-
}
|
|
8601
|
-
}
|
|
8602
|
-
}
|
|
8603
|
-
const validFormIds = collectIds2(data.forms, "forms");
|
|
8604
|
-
const validCalendarIds = collectIds2(data.calendars, "calendars");
|
|
8605
|
-
const validSurveyIds = collectIds2(data.surveys, "surveys");
|
|
8606
|
-
const findings = [];
|
|
8607
|
-
for (const ref of refs) {
|
|
8608
|
-
let valid = false;
|
|
8609
|
-
let extraMsg = "";
|
|
8610
|
-
switch (ref.kind) {
|
|
8611
|
-
case "pipeline":
|
|
8612
|
-
valid = validPipelineIds.has(ref.id);
|
|
8613
|
-
break;
|
|
8614
|
-
case "stage":
|
|
8615
|
-
valid = validStageIds.has(ref.id);
|
|
8616
|
-
break;
|
|
8617
|
-
case "custom_field":
|
|
8618
|
-
valid = validCustomFieldIds.has(ref.id);
|
|
8619
|
-
break;
|
|
8620
|
-
case "user":
|
|
8621
|
-
valid = validUserIds.has(ref.id);
|
|
8622
|
-
break;
|
|
8623
|
-
case "workflow":
|
|
8624
|
-
valid = validWorkflowIds.has(ref.id);
|
|
8625
|
-
if (valid && ref.id === workflowId) {
|
|
8626
|
-
extraMsg = " (self-reference)";
|
|
8627
|
-
}
|
|
8628
|
-
break;
|
|
8629
|
-
case "form":
|
|
8630
|
-
valid = validFormIds.has(ref.id);
|
|
8631
|
-
break;
|
|
8632
|
-
case "calendar":
|
|
8633
|
-
valid = validCalendarIds.has(ref.id);
|
|
8634
|
-
break;
|
|
8635
|
-
case "survey":
|
|
8636
|
-
valid = validSurveyIds.has(ref.id);
|
|
8637
|
-
break;
|
|
8638
|
-
}
|
|
8639
|
-
if (!valid) {
|
|
8640
|
-
findings.push({
|
|
8641
|
-
severity: "error",
|
|
8642
|
-
category: ref.kind,
|
|
8643
|
-
id: ref.id,
|
|
8644
|
-
where: ref.where,
|
|
8645
|
-
message: `${ref.kind} id "${ref.id}" does not exist in this location \u2014 GHL will silently skip this action and all subsequent actions when the workflow runs.`
|
|
8646
|
-
});
|
|
8647
|
-
} else if (extraMsg) {
|
|
8648
|
-
findings.push({
|
|
8649
|
-
severity: "warning",
|
|
8650
|
-
category: ref.kind,
|
|
8651
|
-
id: ref.id,
|
|
8652
|
-
where: ref.where,
|
|
8653
|
-
message: `${ref.kind} id "${ref.id}" is valid${extraMsg}.`
|
|
8654
|
-
});
|
|
8655
|
-
}
|
|
8656
|
-
}
|
|
8838
|
+
for (const t of Array.isArray(workflow.triggers) ? workflow.triggers : []) extractFromTrigger(t, refs);
|
|
8839
|
+
for (const a of Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : []) extractFromAction(a, refs);
|
|
8840
|
+
if (refs.length === 0)
|
|
8841
|
+
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, findings: [] });
|
|
8842
|
+
const needWorkflows = refs.some((r) => r.kind === "workflow");
|
|
8843
|
+
const catalog = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
|
|
8844
|
+
const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog.ids, complete: catalog.complete });
|
|
8845
|
+
const findings = checkRefs(refs, workflowId, lookups);
|
|
8657
8846
|
const report = {
|
|
8658
8847
|
workflowId,
|
|
8659
8848
|
workflowName: workflow.name,
|
|
@@ -8668,6 +8857,69 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8668
8857
|
}
|
|
8669
8858
|
}
|
|
8670
8859
|
);
|
|
8860
|
+
server2.tool(
|
|
8861
|
+
"audit_workflows",
|
|
8862
|
+
"Account-wide silent-failure audit: scans EVERY workflow in the current location for references to pipelines/stages/custom-fields/users/workflows/forms/calendars/surveys that don't exist \u2014 the GHL bug where one bad ID silently kills that action and all actions after it. Returns a prioritized report of what's broken, what couldn't be scanned, and what couldn't be fully verified. Conservative: never reports a false break (uncertain checks are 'unverified', not 'broken'). Read-only.",
|
|
8863
|
+
{},
|
|
8864
|
+
async () => {
|
|
8865
|
+
try {
|
|
8866
|
+
const locationId2 = client.defaultLocationId;
|
|
8867
|
+
const catalog = await fullWorkflowCatalog(builderClient);
|
|
8868
|
+
if (catalog.rows.length === 0)
|
|
8869
|
+
return jsonResponse({ location_id: locationId2, summary: { workflows_total: 0, workflows_scanned: 0, status: "ok", message: "No workflows found in this location." }, workflows_with_issues: [], unscannable: [] });
|
|
8870
|
+
const lookups = await fetchAndBuildLookups(client, builderClient, locationId2, { ids: catalog.ids, complete: catalog.complete });
|
|
8871
|
+
const SCAN_CAP = 300;
|
|
8872
|
+
const toScan = catalog.rows.slice(0, SCAN_CAP);
|
|
8873
|
+
const CONCURRENCY = 6;
|
|
8874
|
+
const results = [];
|
|
8875
|
+
const unscannable = [];
|
|
8876
|
+
let zeroRefCount = 0;
|
|
8877
|
+
for (let i = 0; i < toScan.length; i += CONCURRENCY) {
|
|
8878
|
+
const batch = toScan.slice(i, i + CONCURRENCY);
|
|
8879
|
+
await Promise.all(batch.map(async (row) => {
|
|
8880
|
+
try {
|
|
8881
|
+
const wf = await builderClient.getWorkflow(row.id);
|
|
8882
|
+
const refs = auditOneWorkflow(wf, row.id, lookups);
|
|
8883
|
+
if (refs.length === 0) zeroRefCount++;
|
|
8884
|
+
const findings = checkRefs(refs, row.id, lookups);
|
|
8885
|
+
results.push({ id: row.id, name: wf.name ?? row.name, status: wf.status, refs: refs.length, findings });
|
|
8886
|
+
} catch (e) {
|
|
8887
|
+
unscannable.push({ id: row.id, name: row.name, reason: e instanceof Error ? e.message : String(e) });
|
|
8888
|
+
}
|
|
8889
|
+
}));
|
|
8890
|
+
}
|
|
8891
|
+
const withErrors = results.filter((r) => r.findings.some((f) => f.severity === "error")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, errors: r.findings.filter((f) => f.severity === "error") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
8892
|
+
const errorsTotal = withErrors.reduce((n, w) => n + w.errors.length, 0);
|
|
8893
|
+
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
8894
|
+
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
8895
|
+
return jsonResponse({
|
|
8896
|
+
location_id: locationId2,
|
|
8897
|
+
summary: {
|
|
8898
|
+
workflows_total: catalog.rows.length,
|
|
8899
|
+
workflows_scanned: results.length,
|
|
8900
|
+
enumeration_complete: catalog.complete,
|
|
8901
|
+
capped: catalog.rows.length > SCAN_CAP,
|
|
8902
|
+
workflows_with_errors: withErrors.length,
|
|
8903
|
+
errors_total: errorsTotal,
|
|
8904
|
+
workflows_unscannable: unscannable.length,
|
|
8905
|
+
workflows_zero_references: zeroRefCount,
|
|
8906
|
+
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
8907
|
+
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
8908
|
+
},
|
|
8909
|
+
workflows_with_issues: withErrors,
|
|
8910
|
+
unscannable,
|
|
8911
|
+
notes: [
|
|
8912
|
+
...catalog.complete ? [] : ["Workflow catalog exceeded the pagination backstop \u2014 some workflow-id references shown as unverified."],
|
|
8913
|
+
...catalog.rows.length > SCAN_CAP ? [`Only the first ${SCAN_CAP} workflows were scanned (account has ${catalog.rows.length}).`] : [],
|
|
8914
|
+
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
8915
|
+
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
8916
|
+
]
|
|
8917
|
+
});
|
|
8918
|
+
} catch (error) {
|
|
8919
|
+
return errorResponse(error);
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
);
|
|
8671
8923
|
}
|
|
8672
8924
|
|
|
8673
8925
|
// src/version-check.ts
|
|
@@ -8770,6 +9022,10 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
|
|
|
8770
9022
|
if (!registry2) {
|
|
8771
9023
|
return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
|
|
8772
9024
|
}
|
|
9025
|
+
const loadFailure = registry2.getLoadFailure();
|
|
9026
|
+
if (loadFailure) {
|
|
9027
|
+
return { name: "Token registry", status: "fail", detail: loadFailure };
|
|
9028
|
+
}
|
|
8773
9029
|
const locs = registry2.listLocations();
|
|
8774
9030
|
const companies = registry2.listCompanyFirebases();
|
|
8775
9031
|
const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
|
|
@@ -8833,7 +9089,7 @@ function resolveSnapshotAuth(client, registry2, companyIdParam) {
|
|
|
8833
9089
|
const agencyKey = registry2?.getAgencyKey();
|
|
8834
9090
|
if (!agencyKey) {
|
|
8835
9091
|
throw new Error(
|
|
8836
|
-
"Snapshots require an agency/company-scoped API key, and none is registered. This install only has sub-account Private Integration keys.
|
|
9092
|
+
"Snapshots require an agency/company-scoped API key, and none is registered. This install only has sub-account Private Integration keys. Create a Private Integration at the AGENCY level in GHL (Agency Settings > Private Integrations), then run register_agency_key with that key, and retry."
|
|
8837
9093
|
);
|
|
8838
9094
|
}
|
|
8839
9095
|
const storedCompanyId = client.defaultLocationId ? registry2?.getToken(client.defaultLocationId)?.companyId : void 0;
|
|
@@ -9258,28 +9514,336 @@ function registerMetaTools(server2, installedVersion) {
|
|
|
9258
9514
|
);
|
|
9259
9515
|
}
|
|
9260
9516
|
|
|
9517
|
+
// src/cli.ts
|
|
9518
|
+
var import_node_util = require("node:util");
|
|
9519
|
+
var fs5 = __toESM(require("fs"));
|
|
9520
|
+
var path5 = __toESM(require("path"));
|
|
9521
|
+
var import_crypto2 = require("crypto");
|
|
9522
|
+
var EXIT_OK = 0;
|
|
9523
|
+
var EXIT_USAGE = 2;
|
|
9524
|
+
var EXIT_VALIDATION = 3;
|
|
9525
|
+
var EXIT_FS = 4;
|
|
9526
|
+
var USAGE = `Usage: ghl-mcp cli <subcommand> [options]
|
|
9527
|
+
|
|
9528
|
+
Subcommands:
|
|
9529
|
+
register-location Add a sub-account's Private Integration key
|
|
9530
|
+
--location-id <id> GHL Location ID (required)
|
|
9531
|
+
--api-key <pit-...> The sub-account's Private Integration key (required)
|
|
9532
|
+
--name <name> Friendly name (required)
|
|
9533
|
+
--company-id <id> Owning company ID (optional; auto-resolved when validating)
|
|
9534
|
+
--no-validate Skip the live GHL check (air-gapped; local-admin-only)
|
|
9535
|
+
|
|
9536
|
+
register-company-firebase Add a company's workflow-builder (Firebase) credentials
|
|
9537
|
+
--company-id <id> GHL company/agency ID (required; self-corrects to the
|
|
9538
|
+
token's real company when validating)
|
|
9539
|
+
--refresh-token <tok> Firebase refresh token (required)
|
|
9540
|
+
--user-id <uid> Firebase user ID (required)
|
|
9541
|
+
--api-key <AIza...> Firebase API key (optional if the home install has one)
|
|
9542
|
+
--name <name> Friendly company name (optional)
|
|
9543
|
+
--no-validate Skip the live token-mint check
|
|
9544
|
+
|
|
9545
|
+
register-agency-key Store the AGENCY-level API key (snapshots, agency reads)
|
|
9546
|
+
--api-key <pit-...> Agency-level Private Integration key (required)
|
|
9547
|
+
--no-validate Skip the live agency-scope check
|
|
9548
|
+
|
|
9549
|
+
list-locations Print registered locations / companies (never prints keys)
|
|
9550
|
+
|
|
9551
|
+
Exit codes: 0 ok, 2 usage, 3 validation failed, 4 filesystem write failed.
|
|
9552
|
+
Seed while the MCP server is stopped, or restart it afterwards.`;
|
|
9553
|
+
function errLine(msg) {
|
|
9554
|
+
process.stderr.write(msg + "\n");
|
|
9555
|
+
}
|
|
9556
|
+
function preflightWritable() {
|
|
9557
|
+
try {
|
|
9558
|
+
const dir = ensureAppDataDir();
|
|
9559
|
+
const probe = path5.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
|
|
9560
|
+
fs5.writeFileSync(probe, "ok");
|
|
9561
|
+
fs5.unlinkSync(probe);
|
|
9562
|
+
return true;
|
|
9563
|
+
} catch (error) {
|
|
9564
|
+
errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
|
|
9565
|
+
errLine(`Config dir resolves to the parent of: ${tokenRegistryPath()}`);
|
|
9566
|
+
errLine("If you set GHL_MCP_CONFIG_DIR, make sure it is an absolute path on a writable volume.");
|
|
9567
|
+
return false;
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
function restartReminder() {
|
|
9571
|
+
errLine("Note: if the MCP server is currently running, restart it to pick up this change");
|
|
9572
|
+
errLine("(registry writes are last-writer-wins across processes \u2014 seed stopped, or restart after).");
|
|
9573
|
+
}
|
|
9574
|
+
function confirmSaved(registry2) {
|
|
9575
|
+
const err = registry2.getLastSaveError();
|
|
9576
|
+
if (err) {
|
|
9577
|
+
errLine(`Failed to write registry: ${err}`);
|
|
9578
|
+
return false;
|
|
9579
|
+
}
|
|
9580
|
+
return true;
|
|
9581
|
+
}
|
|
9582
|
+
function parse(argv, options, required) {
|
|
9583
|
+
let parsed;
|
|
9584
|
+
try {
|
|
9585
|
+
parsed = (0, import_node_util.parseArgs)({ args: argv, options, strict: true, allowPositionals: false });
|
|
9586
|
+
} catch (error) {
|
|
9587
|
+
return { usageError: error instanceof Error ? error.message : String(error) };
|
|
9588
|
+
}
|
|
9589
|
+
for (const key of required) {
|
|
9590
|
+
const v = parsed.values[key];
|
|
9591
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
9592
|
+
return { usageError: `Missing required option: --${key}` };
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
return parsed;
|
|
9596
|
+
}
|
|
9597
|
+
async function cmdRegisterLocation(argv, registry2) {
|
|
9598
|
+
const p = parse(
|
|
9599
|
+
argv,
|
|
9600
|
+
{
|
|
9601
|
+
"location-id": { type: "string" },
|
|
9602
|
+
"api-key": { type: "string" },
|
|
9603
|
+
name: { type: "string" },
|
|
9604
|
+
"company-id": { type: "string" },
|
|
9605
|
+
"no-validate": { type: "boolean" }
|
|
9606
|
+
},
|
|
9607
|
+
["location-id", "api-key", "name"]
|
|
9608
|
+
);
|
|
9609
|
+
if ("usageError" in p) {
|
|
9610
|
+
errLine(p.usageError);
|
|
9611
|
+
errLine(USAGE);
|
|
9612
|
+
return EXIT_USAGE;
|
|
9613
|
+
}
|
|
9614
|
+
const locationId2 = p.values["location-id"].trim();
|
|
9615
|
+
const apiKey2 = p.values["api-key"].trim();
|
|
9616
|
+
let name = p.values.name.trim();
|
|
9617
|
+
let companyId = p.values["company-id"]?.trim() || void 0;
|
|
9618
|
+
if (!p.values["no-validate"]) {
|
|
9619
|
+
const client = new GHLClient({ apiKey: apiKey2, locationId: locationId2 });
|
|
9620
|
+
try {
|
|
9621
|
+
const result = await client.get(`/locations/${locationId2}`);
|
|
9622
|
+
const loc = result.location ?? result;
|
|
9623
|
+
if (typeof loc?.name === "string" && loc.name) name = loc.name;
|
|
9624
|
+
if (!companyId && typeof loc?.companyId === "string") companyId = loc.companyId;
|
|
9625
|
+
} catch (error) {
|
|
9626
|
+
errLine(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9627
|
+
errLine("The key must be a Private Integration created INSIDE this sub-account, and the");
|
|
9628
|
+
errLine("Location ID must match. Use --no-validate only if this machine cannot reach GHL.");
|
|
9629
|
+
return EXIT_VALIDATION;
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9633
|
+
try {
|
|
9634
|
+
registry2.registerLocation(locationId2, name, apiKey2, companyId);
|
|
9635
|
+
} catch (error) {
|
|
9636
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9637
|
+
return EXIT_FS;
|
|
9638
|
+
}
|
|
9639
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9640
|
+
process.stdout.write(
|
|
9641
|
+
`Registered location: ${name} (${locationId2})${companyId ? ` company ${companyId}` : ""} \u2192 ${tokenRegistryPath()}
|
|
9642
|
+
`
|
|
9643
|
+
);
|
|
9644
|
+
restartReminder();
|
|
9645
|
+
return EXIT_OK;
|
|
9646
|
+
}
|
|
9647
|
+
async function cmdRegisterCompanyFirebase(argv, registry2) {
|
|
9648
|
+
const p = parse(
|
|
9649
|
+
argv,
|
|
9650
|
+
{
|
|
9651
|
+
"company-id": { type: "string" },
|
|
9652
|
+
"refresh-token": { type: "string" },
|
|
9653
|
+
"user-id": { type: "string" },
|
|
9654
|
+
"api-key": { type: "string" },
|
|
9655
|
+
name: { type: "string" },
|
|
9656
|
+
"no-validate": { type: "boolean" }
|
|
9657
|
+
},
|
|
9658
|
+
["company-id", "refresh-token", "user-id"]
|
|
9659
|
+
);
|
|
9660
|
+
if ("usageError" in p) {
|
|
9661
|
+
errLine(p.usageError);
|
|
9662
|
+
errLine(USAGE);
|
|
9663
|
+
return EXIT_USAGE;
|
|
9664
|
+
}
|
|
9665
|
+
const typedCompanyId = p.values["company-id"].trim();
|
|
9666
|
+
const refreshToken = p.values["refresh-token"].trim();
|
|
9667
|
+
const userId = p.values["user-id"].trim();
|
|
9668
|
+
const name = p.values.name?.trim();
|
|
9669
|
+
const apiKey2 = p.values["api-key"]?.trim() || process.env.GHL_FIREBASE_API_KEY || registry2.getFirebase()?.apiKey;
|
|
9670
|
+
if (!apiKey2) {
|
|
9671
|
+
errLine("No Firebase API key available. Pass --api-key (starts with 'AIza'), or seed the home");
|
|
9672
|
+
errLine("Firebase first (the key is identical across GHL accounts).");
|
|
9673
|
+
return EXIT_USAGE;
|
|
9674
|
+
}
|
|
9675
|
+
let canonicalCompanyId = typedCompanyId;
|
|
9676
|
+
if (!p.values["no-validate"]) {
|
|
9677
|
+
const fb = await validateFirebase(apiKey2, refreshToken);
|
|
9678
|
+
if (!fb.ok) {
|
|
9679
|
+
errLine(`Firebase credentials rejected: ${fb.error}`);
|
|
9680
|
+
errLine("Capture fresh values from a browser session logged into THIS company's GHL:");
|
|
9681
|
+
errLine("https://elitedcs.com/ghl-mcp-firebase");
|
|
9682
|
+
return EXIT_VALIDATION;
|
|
9683
|
+
}
|
|
9684
|
+
if (fb.companyId && fb.companyId !== typedCompanyId) {
|
|
9685
|
+
canonicalCompanyId = fb.companyId;
|
|
9686
|
+
errLine(
|
|
9687
|
+
`Note: stored under company ${fb.companyId} (the ID this token actually authenticates as), not ${typedCompanyId}.`
|
|
9688
|
+
);
|
|
9689
|
+
}
|
|
9690
|
+
} else {
|
|
9691
|
+
errLine("Warning: --no-validate stores the typed company ID verbatim. If workflow-builder calls");
|
|
9692
|
+
errLine("401 after seeding, re-run WITHOUT --no-validate so the ID self-corrects from the token.");
|
|
9693
|
+
}
|
|
9694
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9695
|
+
try {
|
|
9696
|
+
registry2.setCompanyFirebase(canonicalCompanyId, {
|
|
9697
|
+
apiKey: apiKey2,
|
|
9698
|
+
refreshToken,
|
|
9699
|
+
userId,
|
|
9700
|
+
...name ? { name } : {}
|
|
9701
|
+
});
|
|
9702
|
+
} catch (error) {
|
|
9703
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9704
|
+
return EXIT_FS;
|
|
9705
|
+
}
|
|
9706
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9707
|
+
process.stdout.write(
|
|
9708
|
+
`Registered company Firebase: ${name ?? canonicalCompanyId} (${canonicalCompanyId}) \u2192 ${tokenRegistryPath()}
|
|
9709
|
+
`
|
|
9710
|
+
);
|
|
9711
|
+
restartReminder();
|
|
9712
|
+
return EXIT_OK;
|
|
9713
|
+
}
|
|
9714
|
+
async function cmdRegisterAgencyKey(argv, registry2) {
|
|
9715
|
+
const p = parse(
|
|
9716
|
+
argv,
|
|
9717
|
+
{ "api-key": { type: "string" }, "no-validate": { type: "boolean" } },
|
|
9718
|
+
["api-key"]
|
|
9719
|
+
);
|
|
9720
|
+
if ("usageError" in p) {
|
|
9721
|
+
errLine(p.usageError);
|
|
9722
|
+
errLine(USAGE);
|
|
9723
|
+
return EXIT_USAGE;
|
|
9724
|
+
}
|
|
9725
|
+
const apiKey2 = p.values["api-key"].trim();
|
|
9726
|
+
if (!p.values["no-validate"]) {
|
|
9727
|
+
const client = new GHLClient({ apiKey: apiKey2 });
|
|
9728
|
+
try {
|
|
9729
|
+
const probe = await client.get("/locations/search", { params: { limit: 1, skip: 0 } });
|
|
9730
|
+
const locations = probe?.locations;
|
|
9731
|
+
if (!Array.isArray(locations)) {
|
|
9732
|
+
throw new Error("response is not an agency location-search envelope (no locations array)");
|
|
9733
|
+
}
|
|
9734
|
+
} catch (error) {
|
|
9735
|
+
errLine(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9736
|
+
errLine("The key must be a Private Integration created at the AGENCY level (Agency Settings >");
|
|
9737
|
+
errLine("Private Integrations), with the locations scope enabled.");
|
|
9738
|
+
return EXIT_VALIDATION;
|
|
9739
|
+
}
|
|
9740
|
+
}
|
|
9741
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9742
|
+
try {
|
|
9743
|
+
registry2.setAgencyKey(apiKey2);
|
|
9744
|
+
} catch (error) {
|
|
9745
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9746
|
+
return EXIT_FS;
|
|
9747
|
+
}
|
|
9748
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9749
|
+
process.stdout.write(`Registered agency key: ${apiKey2.substring(0, 12)}... \u2192 ${tokenRegistryPath()}
|
|
9750
|
+
`);
|
|
9751
|
+
restartReminder();
|
|
9752
|
+
return EXIT_OK;
|
|
9753
|
+
}
|
|
9754
|
+
function cmdListLocations(registry2) {
|
|
9755
|
+
const locs = registry2.listLocations();
|
|
9756
|
+
const companies = registry2.listCompanyFirebases();
|
|
9757
|
+
const out = {
|
|
9758
|
+
registryPath: tokenRegistryPath(),
|
|
9759
|
+
locations: locs,
|
|
9760
|
+
agencyKey: registry2.getAgencyKey() ? "registered" : "not registered",
|
|
9761
|
+
homeFirebase: registry2.getFirebase() ? "registered" : "not registered",
|
|
9762
|
+
// names/ids ONLY — no keys, tokens, or user ids (design contract).
|
|
9763
|
+
companyFirebases: companies.map(({ companyId, name }) => ({ companyId, ...name ? { name } : {} }))
|
|
9764
|
+
};
|
|
9765
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
9766
|
+
return EXIT_OK;
|
|
9767
|
+
}
|
|
9768
|
+
async function runCli(subcommand, argv) {
|
|
9769
|
+
let registry2;
|
|
9770
|
+
try {
|
|
9771
|
+
registry2 = new TokenRegistry();
|
|
9772
|
+
} catch (error) {
|
|
9773
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
9774
|
+
return EXIT_USAGE;
|
|
9775
|
+
}
|
|
9776
|
+
const loadFailure = registry2.getLoadFailure();
|
|
9777
|
+
if (loadFailure) {
|
|
9778
|
+
errLine(loadFailure);
|
|
9779
|
+
return EXIT_FS;
|
|
9780
|
+
}
|
|
9781
|
+
switch (subcommand) {
|
|
9782
|
+
case "register-location":
|
|
9783
|
+
return cmdRegisterLocation(argv, registry2);
|
|
9784
|
+
case "register-company-firebase":
|
|
9785
|
+
return cmdRegisterCompanyFirebase(argv, registry2);
|
|
9786
|
+
case "register-agency-key":
|
|
9787
|
+
return cmdRegisterAgencyKey(argv, registry2);
|
|
9788
|
+
case "list-locations":
|
|
9789
|
+
return cmdListLocations(registry2);
|
|
9790
|
+
case void 0:
|
|
9791
|
+
case "help":
|
|
9792
|
+
case "--help":
|
|
9793
|
+
case "-h":
|
|
9794
|
+
process.stdout.write(USAGE + "\n");
|
|
9795
|
+
return subcommand === void 0 ? EXIT_USAGE : EXIT_OK;
|
|
9796
|
+
default:
|
|
9797
|
+
errLine(`Unknown subcommand: ${subcommand}`);
|
|
9798
|
+
errLine(USAGE);
|
|
9799
|
+
return EXIT_USAGE;
|
|
9800
|
+
}
|
|
9801
|
+
}
|
|
9802
|
+
|
|
9261
9803
|
// src/index.ts
|
|
9262
|
-
var
|
|
9804
|
+
var bundledPkg = require_package();
|
|
9805
|
+
var pkg = (() => {
|
|
9806
|
+
try {
|
|
9807
|
+
const onDisk = JSON.parse(
|
|
9808
|
+
fs6.readFileSync(path6.resolve(__dirname, "..", "package.json"), "utf8")
|
|
9809
|
+
);
|
|
9810
|
+
if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
|
|
9811
|
+
return { version: onDisk.version };
|
|
9812
|
+
}
|
|
9813
|
+
} catch {
|
|
9814
|
+
}
|
|
9815
|
+
return bundledPkg;
|
|
9816
|
+
})();
|
|
9263
9817
|
dotenv2.config();
|
|
9818
|
+
{
|
|
9819
|
+
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
9820
|
+
if (configDirOverride && !path6.isAbsolute(configDirOverride)) {
|
|
9821
|
+
process.stderr.write(
|
|
9822
|
+
`[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.
|
|
9823
|
+
`
|
|
9824
|
+
);
|
|
9825
|
+
process.exit(2);
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9264
9828
|
process.on("unhandledRejection", (reason) => {
|
|
9265
9829
|
process.stderr.write(`[ghl-mcp] Unhandled rejection: ${reason}
|
|
9266
9830
|
`);
|
|
9267
9831
|
});
|
|
9268
9832
|
function hardenSecretFilePerms() {
|
|
9269
|
-
const repoDir =
|
|
9833
|
+
const repoDir = path6.resolve(__dirname, "..");
|
|
9270
9834
|
const candidates = [
|
|
9271
|
-
{ file:
|
|
9835
|
+
{ file: path6.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
9272
9836
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
9273
|
-
{ file:
|
|
9837
|
+
{ file: path6.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
9274
9838
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
9275
9839
|
];
|
|
9276
9840
|
for (const { file, mode } of candidates) {
|
|
9277
9841
|
let current;
|
|
9278
9842
|
try {
|
|
9279
|
-
if (!
|
|
9280
|
-
current =
|
|
9843
|
+
if (!fs6.existsSync(file)) continue;
|
|
9844
|
+
current = fs6.statSync(file).mode & 511;
|
|
9281
9845
|
if (current !== mode) {
|
|
9282
|
-
|
|
9846
|
+
fs6.chmodSync(file, mode);
|
|
9283
9847
|
}
|
|
9284
9848
|
} catch (error) {
|
|
9285
9849
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -9493,9 +10057,28 @@ async function checkForUpdates() {
|
|
|
9493
10057
|
}
|
|
9494
10058
|
}
|
|
9495
10059
|
async function main() {
|
|
10060
|
+
if (process.argv[2] === "cli") {
|
|
10061
|
+
process.exit(await runCli(process.argv[3], process.argv.slice(4)));
|
|
10062
|
+
}
|
|
9496
10063
|
await resolveAccessAndRegister();
|
|
9497
10064
|
const transport = new import_stdio.StdioServerTransport();
|
|
9498
10065
|
await server.connect(transport);
|
|
10066
|
+
let shuttingDown = false;
|
|
10067
|
+
const shutdown = async (signal) => {
|
|
10068
|
+
if (shuttingDown) return;
|
|
10069
|
+
shuttingDown = true;
|
|
10070
|
+
process.stderr.write(`[ghl-mcp] ${signal} received \u2014 shutting down.
|
|
10071
|
+
`);
|
|
10072
|
+
const deadline = setTimeout(() => process.exit(0), 3e3);
|
|
10073
|
+
deadline.unref();
|
|
10074
|
+
try {
|
|
10075
|
+
await server.close();
|
|
10076
|
+
} catch {
|
|
10077
|
+
}
|
|
10078
|
+
process.exit(0);
|
|
10079
|
+
};
|
|
10080
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
10081
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
9499
10082
|
if (inBootstrapMode) {
|
|
9500
10083
|
process.stderr.write(`[ghl-mcp] v${pkg.version} connected (bootstrap mode \u2014 only setup_ghl_mcp available).
|
|
9501
10084
|
`);
|
|
@@ -9504,7 +10087,9 @@ async function main() {
|
|
|
9504
10087
|
process.stderr.write(`[ghl-mcp] v${pkg.version} connected. Token registry: ${locCount} location(s).
|
|
9505
10088
|
`);
|
|
9506
10089
|
validateApiKey();
|
|
9507
|
-
|
|
10090
|
+
if (process.env.GHL_MCP_DISABLE_UPDATE_CHECK !== "1") {
|
|
10091
|
+
checkForUpdates();
|
|
10092
|
+
}
|
|
9508
10093
|
}
|
|
9509
10094
|
}
|
|
9510
10095
|
main().catch((error) => {
|