@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/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.32.0",
34
+ version: "3.34.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
- description: "GoHighLevel MCP Server for Claude. 217 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
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=node22 --format=cjs --outfile=dist/index.js --packages=external",
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: ">=22"
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 path5 = __toESM(require("path"));
110
- var fs5 = __toESM(require("fs"));
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(path6, params) {
175
- const url = new URL(path6, GHL_BASE_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, path6, options = {}, attempt = 0) {
186
- const url = this.buildUrl(path6, options.params);
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} ${path6}`);
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} ${path6}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
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, path6, options, attempt + 1);
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} ${path6}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
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, path6, options, attempt + 1);
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} ${path6}
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(path6, options) {
244
- return this.request("GET", path6, options);
243
+ async get(path7, options) {
244
+ return this.request("GET", path7, options);
245
245
  }
246
- async post(path6, options) {
247
- return this.request("POST", path6, options);
246
+ async post(path7, options) {
247
+ return this.request("POST", path7, options);
248
248
  }
249
- async put(path6, options) {
250
- return this.request("PUT", path6, options);
249
+ async put(path7, options) {
250
+ return this.request("PUT", path7, options);
251
251
  }
252
- async patch(path6, options) {
253
- return this.request("PATCH", path6, options);
252
+ async patch(path7, options) {
253
+ return this.request("PATCH", path7, options);
254
254
  }
255
- async delete(path6, options) {
256
- return this.request("DELETE", path6, options);
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
- return ContactSearchResponseSchema.parse(raw);
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
- return await client.get("/conversations/search", {
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
- return await client.get("/opportunities/search", {
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
- return client.get("/invoices/", { params });
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, path6, body) {
4391
+ async function builderRequest(method, path7, body) {
4285
4392
  const headers = await client.buildHeaders();
4286
- const response = await fetch(`${EMAIL_BUILDER_BASE}${path6}`, {
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${path6}
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
- return jsonResponse(result);
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(path6) {
5548
- return client.request("GET", path6);
5657
+ async function internalGet(path7) {
5658
+ return client.request("GET", path7);
5549
5659
  }
5550
- async function internalPost(path6, body) {
5551
- return client.request("POST", path6, body);
5660
+ async function internalPost(path7, body) {
5661
+ return client.request("POST", path7, body);
5552
5662
  }
5553
- async function internalPut(path6, body) {
5554
- return client.request("PUT", path6, body);
5663
+ async function internalPut(path7, body) {
5664
+ return client.request("PUT", path7, body);
5555
5665
  }
5556
- async function internalDelete(path6) {
5557
- return client.request("DELETE", path6);
5666
+ async function internalDelete(path7) {
5667
+ return client.request("DELETE", path7);
5558
5668
  }
5559
- async function funnelRequest(method, path6, body) {
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${path6}`;
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} ${path6}
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, path6, body) {
5973
+ async function formRequest(method, path7, body) {
5864
5974
  const headers = await client.buildHeaders();
5865
- const url = `https://backend.leadconnectorhq.com/forms${path6}`;
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} ${path6}
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 path6 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
5988
- if (formId) path6 += `&formId=${formId}`;
5989
- if (skip) path6 += `&skip=${skip}`;
5990
- const result = await formRequest("GET", path6);
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, path6, body) {
6116
+ async function pipelineRequest(method, path7, body) {
6007
6117
  const headers = await client.buildHeaders();
6008
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path6}`;
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} ${path6}
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, path6, body) {
7688
+ async function smartListRequest(method, path7, body) {
7525
7689
  const headers = await client.buildHeaders();
7526
- const url = `${SMARTLIST_BASE}${path6}`;
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} ${path6}
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, path6) {
7830
+ async function reputationRequest(method, path7) {
7667
7831
  const headers = await client.buildHeaders();
7668
- const response = await fetch(`${REPUTATION_BASE}${path6}`, { method, headers });
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} ${path6}
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(path6, method = "GET", body) {
7950
+ async function membershipRequest(path7, method = "GET", body) {
7787
7951
  const headers = await client.buildHeaders();
7788
- const response = await fetch(`${MEMBERSHIP_BASE}${path6}`, {
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} ${path6}
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
- const triggerActions = Array.isArray(trigger.actions) ? trigger.actions : [];
8388
- for (let i = 0; i < triggerActions.length; i++) {
8389
- const a = triggerActions[i];
8390
- if (a.type === "add_to_workflow" && typeof a.workflow_id === "string") {
8391
- refs.push({
8392
- kind: "workflow",
8393
- id: a.workflow_id,
8394
- where: `${where} \u2192 actions[${i}] (add_to_workflow)`
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
- case "update_contact_field": {
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 (typeof f.field === "string") {
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 (typeof notif.selectedUser === "string" && notif.selectedUser.trim() !== "") {
8422
- refs.push({
8423
- kind: "user",
8424
- id: notif.selectedUser,
8425
- where: `${where} \u2192 notification.selectedUser`
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
- if (typeof attr.assignedTo === "string" && attr.assignedTo.trim() !== "") {
8432
- refs.push({
8433
- kind: "user",
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
- refs.push({
8443
- kind: "workflow",
8444
- id: attr.workflowId,
8445
- where: `${where} \u2192 workflowId`
8446
- });
8447
- }
8448
- const workflowIdArr = Array.isArray(attr.workflow_id) ? attr.workflow_id : [];
8449
- for (let i = 0; i < workflowIdArr.length; i++) {
8450
- const v = workflowIdArr[i];
8451
- if (typeof v === "string") {
8452
- refs.push({
8453
- kind: "workflow",
8454
- id: v,
8455
- where: `${where} \u2192 workflow_id[${i}]`
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 customInputs = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : [];
8463
- for (let i = 0; i < customInputs.length; i++) {
8464
- const c = customInputs[i];
8465
- if (typeof c.value !== "string") continue;
8466
- if (c.filterField === "pipelineId") {
8467
- refs.push({ kind: "pipeline", id: c.value, where: `${where} \u2192 __customInputFields__[${i}].pipelineId` });
8468
- } else if (c.filterField === "pipelineStageId") {
8469
- refs.push({ kind: "stage", id: c.value, where: `${where} \u2192 __customInputFields__[${i}].pipelineStageId` });
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 a 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 this BEFORE publish_workflow when a workflow has been edited, or whenever a published workflow stops behaving as expected. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions without warning. Returns a structured report of valid + missing references.",
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 triggers = Array.isArray(workflow.triggers) ? workflow.triggers : [];
8506
- for (const t of triggers) {
8507
- extractFromTrigger(t, refs);
8508
- }
8509
- const actions = workflow.workflowData?.templates ?? [];
8510
- for (const a of actions) {
8511
- extractFromAction(a, refs);
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. Add the agency key (created at the AGENCY level in GHL Settings > Integrations) to the token registry, then retry."
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 pkg = require_package();
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 = path5.resolve(__dirname, "..");
9833
+ const repoDir = path6.resolve(__dirname, "..");
9270
9834
  const candidates = [
9271
- { file: path5.join(repoDir, "start-mcp.sh"), mode: 448 },
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: path5.join(repoDir, ".ghl-tokens.json"), mode: 384 },
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 (!fs5.existsSync(file)) continue;
9280
- current = fs5.statSync(file).mode & 511;
9843
+ if (!fs6.existsSync(file)) continue;
9844
+ current = fs6.statSync(file).mode & 511;
9281
9845
  if (current !== mode) {
9282
- fs5.chmodSync(file, mode);
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
- checkForUpdates();
10090
+ if (process.env.GHL_MCP_DISABLE_UPDATE_CHECK !== "1") {
10091
+ checkForUpdates();
10092
+ }
9508
10093
  }
9509
10094
  }
9510
10095
  main().catch((error) => {