@elitedcs/ghl-mcp 3.58.0 → 3.60.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
@@ -114,8 +114,8 @@ var init_ghl_client = __esm({
114
114
  Version: version || GHL_API_VERSION
115
115
  };
116
116
  }
117
- buildUrl(path13, params) {
118
- const url = new URL(path13, GHL_BASE_URL);
117
+ buildUrl(path14, params) {
118
+ const url = new URL(path14, GHL_BASE_URL);
119
119
  if (params) {
120
120
  for (const [key, value] of Object.entries(params)) {
121
121
  if (value !== void 0 && value !== null) {
@@ -125,8 +125,8 @@ var init_ghl_client = __esm({
125
125
  }
126
126
  return url.toString();
127
127
  }
128
- async request(method, path13, options = {}, attempt = 0) {
129
- const url = this.buildUrl(path13, options.params);
128
+ async request(method, path14, options = {}, attempt = 0) {
129
+ const url = this.buildUrl(path14, options.params);
130
130
  const headers = this.buildHeaders(options.version);
131
131
  const fetchOptions = {
132
132
  method,
@@ -144,14 +144,14 @@ var init_ghl_client = __esm({
144
144
  } catch (error) {
145
145
  clearTimeout(timeout);
146
146
  if (error instanceof Error && error.name === "AbortError") {
147
- throw new Error(`Request timeout (30s): ${method} ${path13}`);
147
+ throw new Error(`Request timeout (30s): ${method} ${path14}`);
148
148
  }
149
149
  if (!options.noRetry && attempt < MAX_RETRIES) {
150
150
  const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
151
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path13}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
151
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path14}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
152
152
  `);
153
153
  await new Promise((r) => setTimeout(r, delay4));
154
- return this.request(method, path13, options, attempt + 1);
154
+ return this.request(method, path14, options, attempt + 1);
155
155
  }
156
156
  throw error;
157
157
  } finally {
@@ -159,10 +159,10 @@ var init_ghl_client = __esm({
159
159
  }
160
160
  if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
161
161
  const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
162
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path13}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
162
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path14}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
163
163
  `);
164
164
  await new Promise((r) => setTimeout(r, delay4));
165
- return this.request(method, path13, options, attempt + 1);
165
+ return this.request(method, path14, options, attempt + 1);
166
166
  }
167
167
  if (!response.ok) {
168
168
  let errorBody = "";
@@ -171,7 +171,7 @@ var init_ghl_client = __esm({
171
171
  } catch {
172
172
  }
173
173
  throw new Error(
174
- `GHL API Error ${response.status} ${response.statusText}: ${method} ${path13}
174
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path14}
175
175
  ${errorBody}`
176
176
  );
177
177
  }
@@ -183,20 +183,20 @@ ${errorBody}`
183
183
  return { message: text };
184
184
  }
185
185
  }
186
- async get(path13, options) {
187
- return this.request("GET", path13, options);
186
+ async get(path14, options) {
187
+ return this.request("GET", path14, options);
188
188
  }
189
- async post(path13, options) {
190
- return this.request("POST", path13, options);
189
+ async post(path14, options) {
190
+ return this.request("POST", path14, options);
191
191
  }
192
- async put(path13, options) {
193
- return this.request("PUT", path13, options);
192
+ async put(path14, options) {
193
+ return this.request("PUT", path14, options);
194
194
  }
195
- async patch(path13, options) {
196
- return this.request("PATCH", path13, options);
195
+ async patch(path14, options) {
196
+ return this.request("PATCH", path14, options);
197
197
  }
198
- async delete(path13, options) {
199
- return this.request("DELETE", path13, options);
198
+ async delete(path14, options) {
199
+ return this.request("DELETE", path14, options);
200
200
  }
201
201
  /**
202
202
  * Helper: resolves locationId from args or falls back to default
@@ -1270,9 +1270,9 @@ function planFromPayload(payload) {
1270
1270
  return payload?.plan === "command-os" ? "command-os" : "mcp";
1271
1271
  }
1272
1272
  function legacyDeviceFingerprint() {
1273
- const os6 = require("node:os");
1273
+ const os7 = require("node:os");
1274
1274
  const crypto5 = require("node:crypto");
1275
- const raw = `${os6.hostname()}:${os6.userInfo().username}:${os6.platform()}:${os6.arch()}`;
1275
+ const raw = `${os7.hostname()}:${os7.userInfo().username}:${os7.platform()}:${os7.arch()}`;
1276
1276
  return crypto5.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1277
1277
  }
1278
1278
  function deviceFingerprint(opts) {
@@ -1508,12 +1508,16 @@ function registerSetupTool(server2, pkgVersion) {
1508
1508
  if (pkgVersion) setupPkgVersion = pkgVersion;
1509
1509
  server2.tool(
1510
1510
  "setup_ghl_mcp",
1511
- "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 233 tools (179 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
1511
+ "First-run setup AND license upgrades for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file; restart Claude once after it completes. UPGRADING an existing install (e.g. free key to paid)? Supply ONLY email + license_key \u2014 the GHL credentials and any Workflow Builder unlock already saved on this machine are reused and kept; do not re-enter ghl_api_key or ghl_location_id. No license yet? Paid ($97/mo) at https://ghlcommand.com \u2014 or a FREE read-only key, instantly, at https://ghlcommand.com/free.",
1512
1512
  {
1513
1513
  email: import_zod42.z.string().email().describe("Email used at purchase."),
1514
1514
  license_key: import_zod42.z.string().min(20).describe("License key from your purchase email."),
1515
- ghl_api_key: import_zod42.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
1516
- ghl_location_id: import_zod42.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
1515
+ // v3.60.0: optional AS A PAIR for the one-line upgrade. Supply both, or
1516
+ // neither (reuses the pair saved on this machine). Never one alone
1517
+ // mixing a fresh key with a stale stored location (or vice versa) would
1518
+ // silently persist a mismatched tuple (Codex-agreed atomic-pair rule).
1519
+ ghl_api_key: import_zod42.z.string().min(10).optional().describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations. OMIT together with ghl_location_id to reuse the pair already saved on this machine (upgrades)."),
1520
+ ghl_location_id: import_zod42.z.string().min(10).optional().describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard. OMIT together with ghl_api_key to reuse the saved pair (upgrades)."),
1517
1521
  ghl_company_id: import_zod42.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
1518
1522
  // v3.25.0: one-paste shortcut. Run `auto_capture_firebase_script` first;
1519
1523
  // it returns a console script that fills the clipboard with this exact
@@ -1531,9 +1535,40 @@ function registerSetupTool(server2, pkgVersion) {
1531
1535
  emit("setup_failed", { pkgVersion: setupPkgVersion, reasonCode: LICENSE_REASON_MAP[lic.reason] });
1532
1536
  return { content: [{ type: "text", text: `License check failed: ${lic.error}
1533
1537
 
1534
- Purchase a license at https://elitedcs.com/ghl-mcp-server or contact support.` }], isError: true };
1538
+ Purchase a license at https://ghlcommand.com \u2014 or get a FREE read-only key instantly at https://ghlcommand.com/free. Stuck? Reply to any email from us or write support@ghlcommand.com.` }], isError: true };
1535
1539
  }
1536
- const ghl = await validateGhl(args.ghl_api_key, args.ghl_location_id);
1540
+ const priorForPair = readCredentials();
1541
+ const suppliedApiKey = args.ghl_api_key?.trim();
1542
+ const suppliedLocationId = args.ghl_location_id?.trim();
1543
+ if (suppliedApiKey && !suppliedLocationId || !suppliedApiKey && suppliedLocationId) {
1544
+ return {
1545
+ content: [{
1546
+ type: "text",
1547
+ text: "Provide BOTH ghl_api_key and ghl_location_id, or NEITHER (to reuse the pair already saved on this machine). Mixing one new value with one saved value is not allowed \u2014 they are validated and stored as a pair."
1548
+ }],
1549
+ isError: true
1550
+ };
1551
+ }
1552
+ let ghlApiKey;
1553
+ let ghlLocationId;
1554
+ let usedStoredPair = false;
1555
+ if (suppliedApiKey && suppliedLocationId) {
1556
+ ghlApiKey = suppliedApiKey;
1557
+ ghlLocationId = suppliedLocationId;
1558
+ } else if (priorForPair?.ghl_api_key && priorForPair?.ghl_location_id) {
1559
+ ghlApiKey = priorForPair.ghl_api_key;
1560
+ ghlLocationId = priorForPair.ghl_location_id;
1561
+ usedStoredPair = true;
1562
+ } else {
1563
+ return {
1564
+ content: [{
1565
+ type: "text",
1566
+ text: 'No GHL credentials supplied and none saved on this machine yet. For a first-time setup you need both:\n ghl_api_key \u2014 a Private Integration key (pit-...) created INSIDE your sub-account: Settings > Integrations > Private Integrations (click "Select all" on scopes, copy the WHOLE key)\n ghl_location_id \u2014 the part of your GHL URL after /location/\nFull walkthrough: https://ghlcommand.com/start'
1567
+ }],
1568
+ isError: true
1569
+ };
1570
+ }
1571
+ const ghl = await validateGhl(ghlApiKey, ghlLocationId);
1537
1572
  if (!ghl.ok) {
1538
1573
  emit("setup_failed", { pkgVersion: setupPkgVersion, reasonCode: ghl.reason });
1539
1574
  return { content: [{ type: "text", text: `GHL credential check failed: ${ghl.error}` }], isError: true };
@@ -1573,21 +1608,34 @@ Purchase a license at https://elitedcs.com/ghl-mcp-server or contact support.` }
1573
1608
  if (!fb.ok) {
1574
1609
  workflowBuilderNote = `
1575
1610
 
1576
- Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builder. Re-run setup_ghl_mcp later with fresh Firebase fields (use auto_capture_firebase_script for a one-paste flow) to enable it.`;
1611
+ Note: Firebase credentials rejected (${fb.error}).`;
1577
1612
  } else {
1578
1613
  workflowBuilderEnabled = true;
1579
1614
  }
1580
1615
  }
1616
+ const prior = priorForPair;
1617
+ const priorFirebase = prior?.ghl_user_id && prior?.ghl_firebase_api_key && prior?.ghl_firebase_refresh_token ? {
1618
+ userId: prior.ghl_user_id,
1619
+ fbApi: prior.ghl_firebase_api_key,
1620
+ fbRefresh: prior.ghl_firebase_refresh_token
1621
+ } : null;
1622
+ const finalFirebase = workflowBuilderEnabled ? { userId: resolvedUserId.trim(), fbApi: resolvedFbApi.trim(), fbRefresh: resolvedFbRefresh.trim() } : priorFirebase;
1623
+ if (!workflowBuilderEnabled && wantsWorkflowBuilder && priorFirebase) {
1624
+ workflowBuilderNote += ` KEPT your existing working Workflow Builder credentials \u2014 nothing was lost. To refresh them later, run capture_firebase_interactive.`;
1625
+ } else if (!workflowBuilderEnabled && wantsWorkflowBuilder) {
1626
+ workflowBuilderNote += ` Saved without Workflow Builder. Run capture_firebase_interactive (one browser login) to finish the install.`;
1627
+ }
1628
+ const builderActive = finalFirebase !== null;
1581
1629
  writeCredentials({
1582
1630
  license_key: args.license_key.trim(),
1583
1631
  email: args.email.trim(),
1584
1632
  verified_at: (/* @__PURE__ */ new Date()).toISOString(),
1585
- ghl_api_key: args.ghl_api_key.trim(),
1586
- ghl_location_id: args.ghl_location_id.trim(),
1587
- ghl_company_id: args.ghl_company_id?.trim() || void 0,
1588
- ghl_user_id: workflowBuilderEnabled ? resolvedUserId?.trim() : void 0,
1589
- ghl_firebase_api_key: workflowBuilderEnabled ? resolvedFbApi?.trim() : void 0,
1590
- ghl_firebase_refresh_token: workflowBuilderEnabled ? resolvedFbRefresh?.trim() : void 0,
1633
+ ghl_api_key: ghlApiKey,
1634
+ ghl_location_id: ghlLocationId,
1635
+ ghl_company_id: args.ghl_company_id?.trim() || prior?.ghl_company_id || void 0,
1636
+ ghl_user_id: finalFirebase?.userId,
1637
+ ghl_firebase_api_key: finalFirebase?.fbApi,
1638
+ ghl_firebase_refresh_token: finalFirebase?.fbRefresh,
1591
1639
  // v3.24.0+ attestation. Tied to email + license + device fingerprint;
1592
1640
  // verified on every MCP startup. Closes the hand-crafted creds bypass.
1593
1641
  signed_attestation: lic.signedAttestation
@@ -1600,30 +1648,44 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
1600
1648
  const telemetryLine = telemetryDisabled(process.env) ? "" : `
1601
1649
 
1602
1650
  ${TELEMETRY_DISCLOSURE}`;
1603
- const toolCount = isFree ? workflowBuilderEnabled ? "107" : "89" : workflowBuilderEnabled ? "233" : "179";
1604
- const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
1605
- const wfTip = workflowBuilderEnabled ? "" : isFree ? `
1606
- Unlock the account auditor next (the free tier's best tool): say "Unlock the Workflow Builder" \u2014 one browser login enables audit_workflows, validate_workflow, and the full-detail reads.` : "\nTo enable Workflow Builder later (52 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
1607
- const freeTip = isFree ? "\n\nFree tier: 107 read-only tools (89 before the one-login auditor unlock). Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate, same key upgrades in place): https://ghlcommand.com" : "";
1651
+ const finishedCount = isFree ? "107" : "233";
1652
+ const freeTip = isFree ? `
1653
+
1654
+ Free tier: read-only. Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate) upgrades in place \u2014 same install, you only swap the license key: https://ghlcommand.com` : "";
1655
+ const upgradeLine = usedStoredPair && prior && prior.license_key !== args.license_key.trim() ? `License updated \u2014 everything you set up before is kept.
1656
+ ` : "";
1657
+ const text = builderActive ? [
1658
+ `Setup complete!`,
1659
+ ``,
1660
+ upgradeLine + `License: verified (installs ${lic.installs})${isFree ? " \u2014 FREE read-only tier" : ""}.`,
1661
+ `GHL: connected to "${ghl.locationName}".`,
1662
+ `Workflow Builder: enabled.`,
1663
+ ``,
1664
+ `Credentials saved to: ${credentialsPath()}`,
1665
+ ``,
1666
+ `**Restart Claude once (quit fully and reopen) to load all ${finishedCount} ${isFree ? "read-only " : ""}tools.**`,
1667
+ ``,
1668
+ `After restart, try: "List my GHL contacts" \u2014 or "Audit my workflows" to find what's silently broken.${freeTip}`,
1669
+ workflowBuilderNote,
1670
+ telemetryLine
1671
+ ] : [
1672
+ `Step 1 of 2 done \u2014 one more step finishes the install.`,
1673
+ ``,
1674
+ upgradeLine + `License: verified (installs ${lic.installs})${isFree ? " \u2014 FREE read-only tier" : ""}.`,
1675
+ `GHL: connected to "${ghl.locationName}".`,
1676
+ `Credentials saved to: ${credentialsPath()}`,
1677
+ ``,
1678
+ `**Step 2 \u2014 run \`capture_firebase_interactive\` now.** A Chrome window opens; log into GHL once; the install finishes itself (about 60 seconds). It powers the account auditor${isFree ? "" : " and the workflow/funnel/form builders"} \u2014 the reason you installed this.`,
1679
+ `(No Chrome on this machine? Run \`auto_capture_firebase_script\` for the copy-paste path.)`,
1680
+ ``,
1681
+ `**Then restart Claude once** (quit fully and reopen) and all ${finishedCount} ${isFree ? "read-only " : ""}tools load. One restart total \u2014 do step 2 first.`,
1682
+ ``,
1683
+ `Checkpoint after restart: ask "Audit my workflows".${freeTip}`,
1684
+ workflowBuilderNote,
1685
+ telemetryLine
1686
+ ];
1608
1687
  return {
1609
- content: [{
1610
- type: "text",
1611
- text: [
1612
- `Setup complete!`,
1613
- ``,
1614
- `License: verified (installs ${lic.installs})${isFree ? " \u2014 FREE read-only tier" : ""}.`,
1615
- `GHL: connected to "${ghl.locationName}".`,
1616
- wfLine,
1617
- ``,
1618
- `Credentials saved to: ${credentialsPath()}`,
1619
- ``,
1620
- `**Restart Claude (quit fully and reopen) to load all ${toolCount} ${isFree ? "read-only " : ""}tools.**`,
1621
- ``,
1622
- `After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}${freeTip}`,
1623
- workflowBuilderNote,
1624
- telemetryLine
1625
- ].join("\n")
1626
- }]
1688
+ content: [{ type: "text", text: text.join("\n") }]
1627
1689
  };
1628
1690
  }
1629
1691
  );
@@ -1631,7 +1693,7 @@ Unlock the account auditor next (the free tier's best tool): say "Unlock the Wor
1631
1693
  function registerEnableWorkflowBuilderTool(server2) {
1632
1694
  server2.tool(
1633
1695
  "enable_workflow_builder",
1634
- "Add Firebase credentials to an existing GHL Command install to unlock 54 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 179 to 233 after the next Claude restart.",
1696
+ "Add Firebase credentials to an existing GHL Command install to unlock 52 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 181 to 233 after the next Claude restart.",
1635
1697
  {
1636
1698
  // v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
1637
1699
  // get the console script; the script returns a JSON object that pastes
@@ -2390,12 +2452,409 @@ var init_question_set = __esm({
2390
2452
  }
2391
2453
  });
2392
2454
 
2455
+ // src/config-installer.ts
2456
+ var config_installer_exports = {};
2457
+ __export(config_installer_exports, {
2458
+ EXIT_ABORTED: () => EXIT_ABORTED,
2459
+ EXIT_OK: () => EXIT_OK,
2460
+ EXIT_REFUSED: () => EXIT_REFUSED,
2461
+ EXIT_USAGE: () => EXIT_USAGE,
2462
+ atomicReplace: () => atomicReplace,
2463
+ baselineOf: () => baselineOf,
2464
+ candidateConfigPaths: () => candidateConfigPaths,
2465
+ chooseBackupPath: () => chooseBackupPath,
2466
+ resolveConfigPath: () => resolveConfigPath,
2467
+ resolveSymlinkPolicy: () => resolveSymlinkPolicy,
2468
+ runInstall: () => runInstall
2469
+ });
2470
+ function defaultIO() {
2471
+ return {
2472
+ out: (l) => process.stdout.write(l + "\n"),
2473
+ err: (l) => process.stderr.write(l + "\n"),
2474
+ rename: (from, to) => fs9.renameSync(from, to),
2475
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
2476
+ };
2477
+ }
2478
+ function candidateConfigPaths(opts) {
2479
+ const platform2 = opts?.platform ?? process.platform;
2480
+ const home = opts?.home ?? os4.homedir();
2481
+ const file = "claude_desktop_config.json";
2482
+ const candidates = [];
2483
+ if (platform2 === "darwin") {
2484
+ candidates.push(path8.join(home, "Library", "Application Support", "Claude", file));
2485
+ } else if (platform2 === "win32") {
2486
+ const appData = opts?.appData ?? process.env.APPDATA ?? path8.join(home, "AppData", "Roaming");
2487
+ candidates.push(path8.join(appData, "Claude", file));
2488
+ const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local");
2489
+ const packagesDir = path8.join(localAppData, "Packages");
2490
+ try {
2491
+ for (const entry of fs9.readdirSync(packagesDir)) {
2492
+ if (entry.startsWith("Claude_")) {
2493
+ candidates.push(path8.join(packagesDir, entry, "LocalCache", "Roaming", "Claude", file));
2494
+ }
2495
+ }
2496
+ } catch {
2497
+ }
2498
+ } else {
2499
+ candidates.push(path8.join(home, ".config", "Claude", file));
2500
+ }
2501
+ return candidates;
2502
+ }
2503
+ function resolveConfigPath(explicitPath) {
2504
+ if (explicitPath) {
2505
+ const p = path8.resolve(explicitPath);
2506
+ return { configPath: p, exists: fs9.existsSync(p) };
2507
+ }
2508
+ const candidates = candidateConfigPaths();
2509
+ const existing = candidates.filter((c) => fs9.existsSync(c));
2510
+ if (existing.length > 1) {
2511
+ throw new InstallStop(
2512
+ EXIT_REFUSED,
2513
+ [
2514
+ "More than one Claude settings file was found, and picking the wrong one would break your setup:",
2515
+ ...existing.map((p) => ` ${p}`),
2516
+ "Run the command again with --path and the one Claude actually uses. Nothing was changed."
2517
+ ].join("\n")
2518
+ );
2519
+ }
2520
+ if (existing.length === 1) return { configPath: existing[0], exists: true };
2521
+ return { configPath: candidates[0], exists: false };
2522
+ }
2523
+ function resolveSymlinkPolicy(configPath, explicitPath, home = os4.homedir()) {
2524
+ let st;
2525
+ try {
2526
+ st = fs9.lstatSync(configPath);
2527
+ } catch {
2528
+ return configPath;
2529
+ }
2530
+ if (!st.isSymbolicLink()) return configPath;
2531
+ let real;
2532
+ try {
2533
+ real = fs9.realpathSync(configPath);
2534
+ } catch {
2535
+ throw new InstallStop(
2536
+ EXIT_REFUSED,
2537
+ `${configPath} is a link that points to a file that does not exist. Fix or remove the link, or run again with --path to a real file. Nothing was changed.`
2538
+ );
2539
+ }
2540
+ const rel = path8.relative(path8.resolve(home), real);
2541
+ const outsideHome = rel.startsWith("..") || path8.isAbsolute(rel);
2542
+ if (outsideHome && !explicitPath) {
2543
+ throw new InstallStop(
2544
+ EXIT_REFUSED,
2545
+ `${configPath} is a link pointing outside your home folder (to ${real}). If that is intentional, run again with --path pointing at it directly. Nothing was changed.`
2546
+ );
2547
+ }
2548
+ return real;
2549
+ }
2550
+ function readConfigBytes(configPath) {
2551
+ const bytes = fs9.readFileSync(configPath);
2552
+ const stat = fs9.statSync(configPath);
2553
+ if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 254 && bytes[1] === 255)) {
2554
+ throw new InstallStop(
2555
+ EXIT_REFUSED,
2556
+ "This settings file is saved in an encoding this command doesn't edit (UTF-16). Nothing was changed. Reply to your setup email and we'll sort it out."
2557
+ );
2558
+ }
2559
+ const hadBom = bytes.length >= 3 && bytes.subarray(0, 3).equals(BOM_UTF8);
2560
+ const text = (hadBom ? bytes.subarray(3) : bytes).toString("utf8");
2561
+ return { text, hadBom, bytes, stat };
2562
+ }
2563
+ function isPlainObject(v) {
2564
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2565
+ }
2566
+ function wrongShapeStop() {
2567
+ return new InstallStop(
2568
+ EXIT_REFUSED,
2569
+ "This settings file doesn't have the layout Claude uses, so this command won't rewrite it. Nothing was changed. Reply to your setup email and we'll sort it out."
2570
+ );
2571
+ }
2572
+ function parseTiered(text) {
2573
+ if (text.trim() === "") return { tier: 1, root: {} };
2574
+ try {
2575
+ const strict = JSON.parse(text);
2576
+ if (!isPlainObject(strict)) throw wrongShapeStop();
2577
+ return { tier: 1, root: strict };
2578
+ } catch (e) {
2579
+ if (e instanceof InstallStop) throw e;
2580
+ }
2581
+ try {
2582
+ const tolerant = import_json5.default.parse(text);
2583
+ if (!isPlainObject(tolerant)) throw wrongShapeStop();
2584
+ return { tier: 2, root: tolerant };
2585
+ } catch (e) {
2586
+ if (e instanceof InstallStop) throw e;
2587
+ }
2588
+ return { tier: 3, root: null };
2589
+ }
2590
+ function entriesEqual(a) {
2591
+ if (!isPlainObject(a)) return false;
2592
+ return a.command === DESIRED_ENTRY.command && Array.isArray(a.args) && a.args.length === DESIRED_ENTRY.args.length && a.args.every((v, i) => v === DESIRED_ENTRY.args[i]);
2593
+ }
2594
+ function mergeGhlEntry(root, force) {
2595
+ const existing = root.mcpServers;
2596
+ if (existing !== void 0 && !isPlainObject(existing)) {
2597
+ throw wrongShapeStop();
2598
+ }
2599
+ const servers = isPlainObject(existing) ? existing : {};
2600
+ const keptServers = Object.keys(servers).filter((k) => k !== SERVER_KEY);
2601
+ let action;
2602
+ if (SERVER_KEY in servers) {
2603
+ if (entriesEqual(servers[SERVER_KEY])) {
2604
+ action = "identical";
2605
+ } else if (!force) {
2606
+ throw new InstallStop(
2607
+ EXIT_REFUSED,
2608
+ "A different GHL Command entry is already in this file. Run again with --force to replace it, or leave it as is. Nothing was changed."
2609
+ );
2610
+ } else {
2611
+ action = "replaced";
2612
+ }
2613
+ } else {
2614
+ action = "added";
2615
+ }
2616
+ servers[SERVER_KEY] = { command: DESIRED_ENTRY.command, args: [...DESIRED_ENTRY.args] };
2617
+ root.mcpServers = servers;
2618
+ return { merged: root, keptServers, action };
2619
+ }
2620
+ function backupStamp(now) {
2621
+ const p = (n, w = 2) => String(n).padStart(w, "0");
2622
+ return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
2623
+ }
2624
+ function chooseBackupPath(configPath, now = /* @__PURE__ */ new Date()) {
2625
+ const base = `${configPath}.bak-${backupStamp(now)}`;
2626
+ if (!fs9.existsSync(base)) return base;
2627
+ for (let i = 2; ; i++) {
2628
+ const candidate = `${base}-${i}`;
2629
+ if (!fs9.existsSync(candidate)) return candidate;
2630
+ }
2631
+ }
2632
+ function writeVerifiedBackup(configPath, originalBytes) {
2633
+ const backupPath = chooseBackupPath(configPath);
2634
+ try {
2635
+ fs9.writeFileSync(backupPath, originalBytes);
2636
+ const readBack = fs9.readFileSync(backupPath);
2637
+ if (!readBack.equals(originalBytes)) {
2638
+ throw new Error("backup read-back did not match");
2639
+ }
2640
+ } catch (e) {
2641
+ try {
2642
+ fs9.rmSync(backupPath, { force: true });
2643
+ } catch {
2644
+ }
2645
+ throw new InstallStop(
2646
+ EXIT_ABORTED,
2647
+ `A safety copy of your settings could not be saved, so nothing was changed. (${e instanceof Error ? e.message : String(e)})`
2648
+ );
2649
+ }
2650
+ return backupPath;
2651
+ }
2652
+ function sha2562(bytes) {
2653
+ return (0, import_node_crypto3.createHash)("sha256").update(bytes).digest("hex");
2654
+ }
2655
+ function baselineOf(bytes, stat) {
2656
+ return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha2562(bytes) };
2657
+ }
2658
+ function assertNotStale(configPath, baseline) {
2659
+ let ok = false;
2660
+ try {
2661
+ const st = fs9.statSync(configPath);
2662
+ if (st.size === baseline.size && st.mtimeMs === baseline.mtimeMs) {
2663
+ ok = sha2562(fs9.readFileSync(configPath)) === baseline.hash;
2664
+ } else {
2665
+ ok = false;
2666
+ }
2667
+ } catch {
2668
+ ok = false;
2669
+ }
2670
+ if (!ok) {
2671
+ throw new InstallStop(
2672
+ EXIT_ABORTED,
2673
+ "Your settings file changed while this command was running, so it stopped without touching it. Run the command again."
2674
+ );
2675
+ }
2676
+ }
2677
+ async function atomicReplace(opts) {
2678
+ const dir = path8.dirname(opts.configPath);
2679
+ const tempPath = path8.join(dir, `.${path8.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}`);
2680
+ const fd = fs9.openSync(tempPath, "w");
2681
+ try {
2682
+ fs9.writeFileSync(fd, opts.newBytes);
2683
+ fs9.fsyncSync(fd);
2684
+ } finally {
2685
+ fs9.closeSync(fd);
2686
+ }
2687
+ try {
2688
+ for (let attempt = 0; ; attempt++) {
2689
+ if (opts.baseline) assertNotStale(opts.configPath, opts.baseline);
2690
+ try {
2691
+ opts.io.rename(tempPath, opts.configPath);
2692
+ break;
2693
+ } catch (e) {
2694
+ const code = e.code ?? "";
2695
+ const retryable = LOCK_ERROR_CODES.has(code) && attempt < RENAME_RETRY_DELAYS_MS.length;
2696
+ if (!retryable) {
2697
+ if (LOCK_ERROR_CODES.has(code)) {
2698
+ throw new InstallStop(
2699
+ EXIT_ABORTED,
2700
+ "Claude is holding this file. Quit Claude completely (right-click the Claude icon by your clock and choose Quit \u2014 on Mac press Cmd+Q), then run this command again. Nothing was changed."
2701
+ );
2702
+ }
2703
+ throw e;
2704
+ }
2705
+ await opts.io.sleep(RENAME_RETRY_DELAYS_MS[attempt]);
2706
+ }
2707
+ }
2708
+ } catch (e) {
2709
+ try {
2710
+ fs9.rmSync(tempPath, { force: true });
2711
+ } catch {
2712
+ }
2713
+ throw e;
2714
+ }
2715
+ try {
2716
+ const dirFd = fs9.openSync(dir, "r");
2717
+ try {
2718
+ fs9.fsyncSync(dirFd);
2719
+ } finally {
2720
+ fs9.closeSync(dirFd);
2721
+ }
2722
+ } catch {
2723
+ }
2724
+ }
2725
+ function successReport(io, args) {
2726
+ io.out("Added GHL Command to Claude Desktop.");
2727
+ io.out("");
2728
+ io.out(` Settings file: ${args.configPath}`);
2729
+ if (args.backupPath) io.out(` Backup saved: ${args.backupPath}`);
2730
+ if (args.tier !== 3 && args.keptServers.length > 0) {
2731
+ io.out(` Kept your other tools: ${args.keptServers.join(", ")}`);
2732
+ }
2733
+ if (args.tier === 2) {
2734
+ io.out("");
2735
+ io.out(`Your settings file was rewritten in a cleaner format. The original is saved at ${args.backupPath}.`);
2736
+ }
2737
+ if (args.tier === 3 && args.hadOriginal) {
2738
+ io.out("");
2739
+ io.out(
2740
+ `GHL Command is ready. If you had other tools connected before, reply to your setup email and we'll reconnect them \u2014 your previous settings are saved at ${args.backupPath}.`
2741
+ );
2742
+ }
2743
+ io.out("");
2744
+ io.out("NEXT STEP \u2014 quit Claude completely, not just the window:");
2745
+ io.out(" Windows: right-click the Claude icon by the clock, choose Quit");
2746
+ io.out(" Mac: Cmd+Q");
2747
+ io.out("Then reopen Claude and run setup with your license key.");
2748
+ }
2749
+ async function runInstall(argv, ioOverride) {
2750
+ const io = { ...defaultIO(), ...ioOverride };
2751
+ let flags;
2752
+ try {
2753
+ flags = (0, import_node_util.parseArgs)({
2754
+ args: argv,
2755
+ options: {
2756
+ "dry-run": { type: "boolean" },
2757
+ "print-only": { type: "boolean" },
2758
+ path: { type: "string" },
2759
+ force: { type: "boolean" }
2760
+ },
2761
+ strict: true,
2762
+ allowPositionals: false
2763
+ }).values;
2764
+ } catch (e) {
2765
+ io.err(e instanceof Error ? e.message : String(e));
2766
+ io.err("Usage: ghl-mcp cli install [--dry-run] [--print-only] [--path <file>] [--force]");
2767
+ return EXIT_USAGE;
2768
+ }
2769
+ try {
2770
+ const resolved2 = resolveConfigPath(flags.path);
2771
+ const configPath = resolveSymlinkPolicy(resolved2.configPath, flags.path !== void 0);
2772
+ const exists = fs9.existsSync(configPath);
2773
+ let read = null;
2774
+ let outcome;
2775
+ if (exists) {
2776
+ read = readConfigBytes(configPath);
2777
+ outcome = parseTiered(read.text);
2778
+ } else {
2779
+ outcome = { tier: 1, root: {} };
2780
+ }
2781
+ const { merged, keptServers, action } = outcome.tier === 3 ? { merged: { mcpServers: { [SERVER_KEY]: { command: DESIRED_ENTRY.command, args: [...DESIRED_ENTRY.args] } } }, keptServers: [], action: "added" } : mergeGhlEntry(outcome.root, flags.force === true);
2782
+ if (action === "identical") {
2783
+ io.out("GHL Command is already set up in Claude Desktop \u2014 nothing to do.");
2784
+ return EXIT_OK;
2785
+ }
2786
+ const newText = JSON.stringify(merged, null, 2) + "\n";
2787
+ const newBytes = read?.hadBom ? Buffer.concat([BOM_UTF8, Buffer.from(newText, "utf8")]) : Buffer.from(newText, "utf8");
2788
+ if (flags["print-only"]) {
2789
+ io.out(newText.trimEnd());
2790
+ return EXIT_OK;
2791
+ }
2792
+ if (flags["dry-run"]) {
2793
+ io.out(`Would edit: ${configPath}${exists ? "" : " (new file)"}`);
2794
+ io.out(`Would ${action === "replaced" ? "replace" : "add"} the "${SERVER_KEY}" entry.`);
2795
+ if (keptServers.length > 0) io.out(`Would keep: ${keptServers.join(", ")}`);
2796
+ if (exists) io.out(`Would back up: ${chooseBackupPath(configPath)}`);
2797
+ if (outcome.tier === 2) io.out("Would rewrite the file in a cleaner format (original kept as the backup).");
2798
+ if (outcome.tier === 3) io.out("Would write a fresh settings file (original kept as the backup).");
2799
+ return EXIT_OK;
2800
+ }
2801
+ const backupPath = exists && read ? writeVerifiedBackup(configPath, read.bytes) : null;
2802
+ if (!exists) fs9.mkdirSync(path8.dirname(configPath), { recursive: true });
2803
+ await atomicReplace({
2804
+ configPath,
2805
+ newBytes,
2806
+ baseline: exists && read ? baselineOf(read.bytes, read.stat) : null,
2807
+ io
2808
+ });
2809
+ successReport(io, { configPath, backupPath, keptServers, tier: outcome.tier, hadOriginal: exists });
2810
+ return EXIT_OK;
2811
+ } catch (e) {
2812
+ if (e instanceof InstallStop) {
2813
+ io.err(e.message);
2814
+ return e.exitCode;
2815
+ }
2816
+ io.err(`Something went wrong and nothing was changed: ${e instanceof Error ? e.message : String(e)}`);
2817
+ return EXIT_ABORTED;
2818
+ }
2819
+ }
2820
+ var fs9, os4, path8, import_node_crypto3, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
2821
+ var init_config_installer = __esm({
2822
+ "src/config-installer.ts"() {
2823
+ "use strict";
2824
+ fs9 = __toESM(require("node:fs"));
2825
+ os4 = __toESM(require("node:os"));
2826
+ path8 = __toESM(require("node:path"));
2827
+ import_node_crypto3 = require("node:crypto");
2828
+ import_node_util = require("node:util");
2829
+ import_json5 = __toESM(require("json5"));
2830
+ SERVER_KEY = "ghl";
2831
+ DESIRED_ENTRY = Object.freeze({
2832
+ command: "npx",
2833
+ args: Object.freeze(["-y", "@elitedcs/ghl-mcp@latest"])
2834
+ });
2835
+ EXIT_OK = 0;
2836
+ EXIT_USAGE = 2;
2837
+ EXIT_REFUSED = 3;
2838
+ EXIT_ABORTED = 4;
2839
+ RENAME_RETRY_DELAYS_MS = [200, 500, 1e3];
2840
+ LOCK_ERROR_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
2841
+ BOM_UTF8 = Buffer.from([239, 187, 191]);
2842
+ InstallStop = class extends Error {
2843
+ constructor(exitCode, message) {
2844
+ super(message);
2845
+ this.exitCode = exitCode;
2846
+ }
2847
+ exitCode;
2848
+ };
2849
+ }
2850
+ });
2851
+
2393
2852
  // package.json
2394
2853
  var require_package = __commonJS({
2395
2854
  "package.json"(exports2, module2) {
2396
2855
  module2.exports = {
2397
2856
  name: "@elitedcs/ghl-mcp",
2398
- version: "3.58.0",
2857
+ version: "3.60.0",
2399
2858
  mcpName: "io.github.drjerryrelth/ghl-command",
2400
2859
  description: "GoHighLevel MCP Server for Claude. 233 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
2401
2860
  main: "dist/index.js",
@@ -2458,6 +2917,7 @@ var require_package = __commonJS({
2458
2917
  dependencies: {
2459
2918
  "@modelcontextprotocol/sdk": "^1.29.0",
2460
2919
  dotenv: "^16.5.0",
2920
+ json5: "^2.2.3",
2461
2921
  "playwright-core": "^1.61.1",
2462
2922
  zod: "^3.24.4"
2463
2923
  },
@@ -2485,7 +2945,7 @@ function launchCommand(port = 7300) {
2485
2945
  return `npx -y @elitedcs/ghl-mcp@latest dashboard --port=${port}`;
2486
2946
  }
2487
2947
  function installingEntry() {
2488
- return path9.join(__dirname, "index.js");
2948
+ return path10.join(__dirname, "index.js");
2489
2949
  }
2490
2950
  function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2491
2951
  const cmd = launchCommand(port);
@@ -2493,12 +2953,12 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2493
2953
  const systemApps = "/Applications";
2494
2954
  let base = home;
2495
2955
  try {
2496
- fs10.accessSync(systemApps, fs10.constants.W_OK);
2956
+ fs11.accessSync(systemApps, fs11.constants.W_OK);
2497
2957
  base = "";
2498
2958
  } catch {
2499
2959
  }
2500
- const appDir = base ? path9.join(home, "Applications", "Command OS.app") : path9.join(systemApps, "Command OS.app");
2501
- const macOSDir = path9.join(appDir, "Contents", "MacOS");
2960
+ const appDir = base ? path10.join(home, "Applications", "Command OS.app") : path10.join(systemApps, "Command OS.app");
2961
+ const macOSDir = path10.join(appDir, "Contents", "MacOS");
2502
2962
  const script = [
2503
2963
  "#!/bin/bash",
2504
2964
  "# GHL Command \u2014 Command OS launcher (regenerate: ghl-mcp install-launcher)",
@@ -2527,13 +2987,13 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2527
2987
  targetPath: appDir,
2528
2988
  humanLocation: base ? "your personal Applications folder at ~/Applications (Finder \u2192 Go \u2192 Home \u2192 Applications)" : "your Applications folder \u2014 open Finder \u2192 Applications and look for \u201CCommand OS\u201D",
2529
2989
  files: [
2530
- { path: path9.join(macOSDir, "command-os"), contents: script, executable: true },
2531
- { path: path9.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
2990
+ { path: path10.join(macOSDir, "command-os"), contents: script, executable: true },
2991
+ { path: path10.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
2532
2992
  ]
2533
2993
  };
2534
2994
  }
2535
2995
  if (platform2 === "win32") {
2536
- const target2 = path9.join(home, "Desktop", "Command OS.cmd");
2996
+ const target2 = path10.join(home, "Desktop", "Command OS.cmd");
2537
2997
  return {
2538
2998
  platform: platform2,
2539
2999
  targetPath: target2,
@@ -2546,7 +3006,7 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2546
3006
  ].join("\r\n"), executable: false }]
2547
3007
  };
2548
3008
  }
2549
- const target = path9.join(home, ".local", "share", "applications", "command-os.desktop");
3009
+ const target = path10.join(home, ".local", "share", "applications", "command-os.desktop");
2550
3010
  return {
2551
3011
  platform: platform2,
2552
3012
  targetPath: target,
@@ -2570,11 +3030,11 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2570
3030
  function installLauncher(argv = []) {
2571
3031
  const portArg = argv.find((a) => a.startsWith("--port="));
2572
3032
  const port = portArg ? Number(portArg.split("=")[1]) : 7300;
2573
- const plan = planLauncher(process.platform, os4.homedir(), port, installingEntry());
3033
+ const plan = planLauncher(process.platform, os5.homedir(), port, installingEntry());
2574
3034
  try {
2575
3035
  for (const f of plan.files) {
2576
- fs10.mkdirSync(path9.dirname(f.path), { recursive: true });
2577
- fs10.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
3036
+ fs11.mkdirSync(path10.dirname(f.path), { recursive: true });
3037
+ fs11.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
2578
3038
  }
2579
3039
  } catch (e) {
2580
3040
  process.stderr.write(`
@@ -2594,13 +3054,13 @@ function installLauncher(argv = []) {
2594
3054
  ].join("\n"));
2595
3055
  return 0;
2596
3056
  }
2597
- var fs10, os4, path9;
3057
+ var fs11, os5, path10;
2598
3058
  var init_launcher = __esm({
2599
3059
  "src/launcher.ts"() {
2600
3060
  "use strict";
2601
- fs10 = __toESM(require("fs"));
2602
- os4 = __toESM(require("os"));
2603
- path9 = __toESM(require("path"));
3061
+ fs11 = __toESM(require("fs"));
3062
+ os5 = __toESM(require("os"));
3063
+ path10 = __toESM(require("path"));
2604
3064
  }
2605
3065
  });
2606
3066
 
@@ -2629,10 +3089,10 @@ function stageBlockedBy(stages, stage) {
2629
3089
  return null;
2630
3090
  }
2631
3091
  function claudeBin() {
2632
- const fallback = path10.join(os5.homedir(), ".local", "bin", "claude");
2633
- const fromPath = (process.env.PATH || "").split(path10.delimiter).map((d) => path10.join(d, "claude")).find((p) => {
3092
+ const fallback = path11.join(os6.homedir(), ".local", "bin", "claude");
3093
+ const fromPath = (process.env.PATH || "").split(path11.delimiter).map((d) => path11.join(d, "claude")).find((p) => {
2634
3094
  try {
2635
- fs11.accessSync(p, fs11.constants.X_OK);
3095
+ fs12.accessSync(p, fs12.constants.X_OK);
2636
3096
  return true;
2637
3097
  } catch {
2638
3098
  return false;
@@ -2640,7 +3100,7 @@ function claudeBin() {
2640
3100
  });
2641
3101
  if (fromPath) return fromPath;
2642
3102
  try {
2643
- fs11.accessSync(fallback, fs11.constants.X_OK);
3103
+ fs12.accessSync(fallback, fs12.constants.X_OK);
2644
3104
  return fallback;
2645
3105
  } catch {
2646
3106
  }
@@ -2689,7 +3149,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
2689
3149
  const locationName = SANDBOX_ALLOWLIST[locationId2] || opts.name || locationId2;
2690
3150
  const spec = STAGE_SPECS[stage];
2691
3151
  if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
2692
- return new Promise((resolve6) => {
3152
+ return new Promise((resolve7) => {
2693
3153
  const bin = claudeBin();
2694
3154
  onEvent({ kind: "status", line: `Starting ${spec.name} on ${locationName} (headless Claude, ${spec.allowedTools.length} tools allowed)\u2026` });
2695
3155
  const child = (0, import_child_process2.spawn)(bin, buildClaudeArgs(spec, locationId2, locationName, opts.context), {
@@ -2735,31 +3195,31 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
2735
3195
  if (code !== 0 && !lastText) {
2736
3196
  const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}` };
2737
3197
  onEvent({ kind: "error", line: outcome2.error });
2738
- resolve6(outcome2);
3198
+ resolve7(outcome2);
2739
3199
  return;
2740
3200
  }
2741
3201
  const outcome = parseResultLine(lastText);
2742
3202
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
2743
3203
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
2744
3204
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
2745
- resolve6(outcome);
3205
+ resolve7(outcome);
2746
3206
  });
2747
3207
  child.on("error", (err) => {
2748
3208
  clearTimeout(timeout);
2749
3209
  const outcome = { ok: false, error: `could not start claude: ${err.message}` };
2750
3210
  onEvent({ kind: "error", line: outcome.error });
2751
- resolve6(outcome);
3211
+ resolve7(outcome);
2752
3212
  });
2753
3213
  });
2754
3214
  }
2755
- var import_child_process2, fs11, os5, path10, SANDBOX_ALLOWLIST, PROTECTED_LOCATIONS, STAGE_SPECS, HUMAN_GATE_STAGES;
3215
+ var import_child_process2, fs12, os6, path11, SANDBOX_ALLOWLIST, PROTECTED_LOCATIONS, STAGE_SPECS, HUMAN_GATE_STAGES;
2756
3216
  var init_stage_runner = __esm({
2757
3217
  "src/stage-runner.ts"() {
2758
3218
  "use strict";
2759
3219
  import_child_process2 = require("child_process");
2760
- fs11 = __toESM(require("fs"));
2761
- os5 = __toESM(require("os"));
2762
- path10 = __toESM(require("path"));
3220
+ fs12 = __toESM(require("fs"));
3221
+ os6 = __toESM(require("os"));
3222
+ path11 = __toESM(require("path"));
2763
3223
  SANDBOX_ALLOWLIST = {
2764
3224
  JrV2p35O3hY2wqhr2c0T: "MCP Testing",
2765
3225
  jHP5wkYRineXDlzAOEbW: "Blueprint Demo"
@@ -3479,11 +3939,11 @@ __export(dashboard_exports, {
3479
3939
  writeOverlay: () => writeOverlay
3480
3940
  });
3481
3941
  function overlayPath() {
3482
- return path11.join(appDataDir(), "intake-overlay.json");
3942
+ return path12.join(appDataDir(), "intake-overlay.json");
3483
3943
  }
3484
3944
  function readOverlay() {
3485
3945
  try {
3486
- const raw = JSON.parse(fs12.readFileSync(overlayPath(), "utf8"));
3946
+ const raw = JSON.parse(fs13.readFileSync(overlayPath(), "utf8"));
3487
3947
  if (raw && typeof raw === "object") return raw;
3488
3948
  } catch {
3489
3949
  }
@@ -3491,7 +3951,7 @@ function readOverlay() {
3491
3951
  }
3492
3952
  function writeOverlay(layer) {
3493
3953
  ensureAppDataDir();
3494
- fs12.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
3954
+ fs13.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
3495
3955
  }
3496
3956
  function recoverStuckStages(state) {
3497
3957
  let recovered = 0;
@@ -3503,11 +3963,11 @@ function recoverStuckStages(state) {
3503
3963
  return { state: { ...state, clients }, recovered };
3504
3964
  }
3505
3965
  function cockpitStatePath() {
3506
- return path11.join(appDataDir(), "cockpit-state.json");
3966
+ return path12.join(appDataDir(), "cockpit-state.json");
3507
3967
  }
3508
3968
  function readCockpitState() {
3509
3969
  try {
3510
- const raw = JSON.parse(fs12.readFileSync(cockpitStatePath(), "utf8"));
3970
+ const raw = JSON.parse(fs13.readFileSync(cockpitStatePath(), "utf8"));
3511
3971
  if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
3512
3972
  } catch {
3513
3973
  }
@@ -3515,7 +3975,7 @@ function readCockpitState() {
3515
3975
  }
3516
3976
  function writeCockpitState(state) {
3517
3977
  ensureAppDataDir();
3518
- fs12.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
3978
+ fs13.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
3519
3979
  }
3520
3980
  function setStage(state, locationId2, stageIndex, status) {
3521
3981
  if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES.length) throw new Error("bad stage index");
@@ -3607,7 +4067,7 @@ function notify(message) {
3607
4067
  `);
3608
4068
  try {
3609
4069
  ensureAppDataDir();
3610
- fs12.appendFileSync(path11.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
4070
+ fs13.appendFileSync(path12.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
3611
4071
  `);
3612
4072
  } catch {
3613
4073
  }
@@ -4308,17 +4768,17 @@ async function runDashboard(argv) {
4308
4768
  const source = args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
4309
4769
  let recorderPatterns = [];
4310
4770
  if (source.kind === "recorder") {
4311
- recorderPatterns = await new Promise((resolve6) => {
4771
+ recorderPatterns = await new Promise((resolve7) => {
4312
4772
  const ls = (0, import_child_process4.spawn)(claudeBin(), ["mcp", "list"], { stdio: ["ignore", "pipe", "ignore"] });
4313
4773
  let buf = "";
4314
4774
  ls.stdout.on("data", (d) => {
4315
4775
  buf += d.toString();
4316
4776
  });
4317
- ls.on("close", () => resolve6(recorderAllowPatterns(buf)));
4318
- ls.on("error", () => resolve6([]));
4777
+ ls.on("close", () => resolve7(recorderAllowPatterns(buf)));
4778
+ ls.on("error", () => resolve7([]));
4319
4779
  setTimeout(() => {
4320
4780
  ls.kill("SIGKILL");
4321
- resolve6(recorderAllowPatterns(buf));
4781
+ resolve7(recorderAllowPatterns(buf));
4322
4782
  }, 9e4).unref?.();
4323
4783
  });
4324
4784
  if (!recorderPatterns.length) {
@@ -4328,7 +4788,7 @@ async function runDashboard(argv) {
4328
4788
  }
4329
4789
  }
4330
4790
  const out = await prefillFromSource(
4331
- (prompt, tools) => new Promise((resolve6, reject) => {
4791
+ (prompt, tools) => new Promise((resolve7, reject) => {
4332
4792
  const argv2 = ["-p", prompt, "--max-turns", "25", "--disallowedTools", tools.deny];
4333
4793
  if (tools.allow) argv2.push("--allowedTools", tools.allow);
4334
4794
  else argv2.push("--allowedTools", "");
@@ -4337,7 +4797,7 @@ async function runDashboard(argv) {
4337
4797
  child.stdout.on("data", (d) => {
4338
4798
  out2 += d.toString();
4339
4799
  });
4340
- child.on("close", () => resolve6(out2));
4800
+ child.on("close", () => resolve7(out2));
4341
4801
  child.on("error", reject);
4342
4802
  setTimeout(() => child.kill("SIGKILL"), 5 * 60 * 1e3).unref?.();
4343
4803
  }),
@@ -4469,13 +4929,13 @@ async function runDashboard(argv) {
4469
4929
  }
4470
4930
  }
4471
4931
  const review = await generateReview(
4472
- (prompt) => new Promise((resolve6, reject) => {
4932
+ (prompt) => new Promise((resolve7, reject) => {
4473
4933
  const child = (0, import_child_process4.spawn)(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", "Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep", "--max-turns", "6"], { stdio: ["ignore", "pipe", "pipe"] });
4474
4934
  let out = "";
4475
4935
  child.stdout.on("data", (d) => {
4476
4936
  out += d.toString();
4477
4937
  });
4478
- child.on("close", () => resolve6(out));
4938
+ child.on("close", () => resolve7(out));
4479
4939
  child.on("error", reject);
4480
4940
  setTimeout(() => child.kill("SIGKILL"), 6 * 60 * 1e3).unref?.();
4481
4941
  }),
@@ -4648,9 +5108,9 @@ async function runDashboard(argv) {
4648
5108
  res.end();
4649
5109
  });
4650
5110
  try {
4651
- await new Promise((resolve6, reject) => {
5111
+ await new Promise((resolve7, reject) => {
4652
5112
  server2.once("error", reject);
4653
- server2.listen(port, "127.0.0.1", resolve6);
5113
+ server2.listen(port, "127.0.0.1", resolve7);
4654
5114
  });
4655
5115
  } catch (e) {
4656
5116
  const msg2 = e?.code === "EADDRINUSE" ? `Port ${port} is busy with another program. Close it, or start Command OS on a different port.` : `Command OS could not start: ${e instanceof Error ? e.message : String(e)}`;
@@ -4676,18 +5136,18 @@ async function runDashboard(argv) {
4676
5136
  if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
4677
5137
  `);
4678
5138
  });
4679
- return await new Promise((resolve6) => {
4680
- const stop = () => server2.close(() => resolve6(0));
5139
+ return await new Promise((resolve7) => {
5140
+ const stop = () => server2.close(() => resolve7(0));
4681
5141
  process.on("SIGINT", stop);
4682
5142
  process.on("SIGTERM", stop);
4683
5143
  });
4684
5144
  }
4685
- var fs12, path11, http, import_child_process3, import_child_process4, osmod, STAGES, ACCOUNT_TYPES, activeRun, UPGRADE_MSG;
5145
+ var fs13, path12, http, import_child_process3, import_child_process4, osmod, STAGES, ACCOUNT_TYPES, activeRun, UPGRADE_MSG;
4686
5146
  var init_dashboard = __esm({
4687
5147
  "src/dashboard.ts"() {
4688
5148
  "use strict";
4689
- fs12 = __toESM(require("fs"));
4690
- path11 = __toESM(require("path"));
5149
+ fs13 = __toESM(require("fs"));
5150
+ path12 = __toESM(require("path"));
4691
5151
  http = __toESM(require("http"));
4692
5152
  import_child_process3 = require("child_process");
4693
5153
  init_credentials_store();
@@ -4722,8 +5182,8 @@ var init_dashboard = __esm({
4722
5182
 
4723
5183
  // src/index.ts
4724
5184
  var dotenv2 = __toESM(require("dotenv"));
4725
- var path12 = __toESM(require("path"));
4726
- var fs13 = __toESM(require("fs"));
5185
+ var path13 = __toESM(require("path"));
5186
+ var fs14 = __toESM(require("fs"));
4727
5187
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4728
5188
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4729
5189
  init_ghl_client();
@@ -8758,16 +9218,16 @@ function registerEmailTools(server2, client) {
8758
9218
  function registerEmailBuilderInternalTools(server2, builderClient) {
8759
9219
  const client = builderClient;
8760
9220
  if (!client) return;
8761
- async function builderRequest(method, path13, body) {
9221
+ async function builderRequest(method, path14, body) {
8762
9222
  const headers = await client.buildHeaders();
8763
- const response = await fetch(`${EMAIL_BUILDER_BASE}${path13}`, {
9223
+ const response = await fetch(`${EMAIL_BUILDER_BASE}${path14}`, {
8764
9224
  method,
8765
9225
  headers,
8766
9226
  body: body ? JSON.stringify(body) : void 0
8767
9227
  });
8768
9228
  if (!response.ok) {
8769
9229
  const text2 = await response.text();
8770
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path13}
9230
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path14}
8771
9231
  ${text2}`);
8772
9232
  }
8773
9233
  const text = await response.text();
@@ -10024,23 +10484,23 @@ var import_zod36 = require("zod");
10024
10484
  function registerFunnelBuilderTools(server2, builderClient) {
10025
10485
  const client = builderClient;
10026
10486
  if (!client) return;
10027
- async function internalGet(path13) {
10028
- return client.request("GET", path13);
10487
+ async function internalGet(path14) {
10488
+ return client.request("GET", path14);
10029
10489
  }
10030
- async function internalPost(path13, body) {
10031
- return client.request("POST", path13, body);
10490
+ async function internalPost(path14, body) {
10491
+ return client.request("POST", path14, body);
10032
10492
  }
10033
- async function internalPut(path13, body) {
10034
- return client.request("PUT", path13, body);
10493
+ async function internalPut(path14, body) {
10494
+ return client.request("PUT", path14, body);
10035
10495
  }
10036
- async function internalDelete(path13) {
10037
- return client.request("DELETE", path13);
10496
+ async function internalDelete(path14) {
10497
+ return client.request("DELETE", path14);
10038
10498
  }
10039
- async function funnelRequest(method, path13, body) {
10499
+ async function funnelRequest(method, path14, body) {
10040
10500
  const headers = await client.buildHeaders();
10041
10501
  headers.Origin = "https://app.gohighlevel.com";
10042
10502
  headers.Referer = "https://app.gohighlevel.com/";
10043
- const url = `https://backend.leadconnectorhq.com/funnels${path13}`;
10503
+ const url = `https://backend.leadconnectorhq.com/funnels${path14}`;
10044
10504
  const options = { method, headers };
10045
10505
  if (body && (method === "POST" || method === "PUT")) {
10046
10506
  options.body = JSON.stringify(body);
@@ -10048,7 +10508,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
10048
10508
  const response = await fetch(url, options);
10049
10509
  if (!response.ok) {
10050
10510
  const text2 = await response.text();
10051
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path13}
10511
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path14}
10052
10512
  ${text2}`);
10053
10513
  }
10054
10514
  const text = await response.text();
@@ -10993,12 +11453,12 @@ var valueCardSchema = import_zod37.z.object({
10993
11453
  function registerPageStudioTools(server2, builderClient) {
10994
11454
  const client = builderClient;
10995
11455
  if (!client) return;
10996
- async function funnelRequest(method, path13) {
11456
+ async function funnelRequest(method, path14) {
10997
11457
  const headers = await client.buildHeaders();
10998
11458
  headers.Origin = "https://app.gohighlevel.com";
10999
11459
  headers.Referer = "https://app.gohighlevel.com/";
11000
- const response = await fetch(`https://backend.leadconnectorhq.com/funnels${path13}`, { method, headers });
11001
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path13}
11460
+ const response = await fetch(`https://backend.leadconnectorhq.com/funnels${path14}`, { method, headers });
11461
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path14}
11002
11462
  ${await response.text()}`);
11003
11463
  const text = await response.text();
11004
11464
  return text ? JSON.parse(text) : {};
@@ -11317,9 +11777,9 @@ function buildUpdateFormPath(formId, locationId2) {
11317
11777
  function buildUpdateFormBody(name, formData) {
11318
11778
  return { name, formData };
11319
11779
  }
11320
- async function formApiRequest(client, method, path13, body) {
11780
+ async function formApiRequest(client, method, path14, body) {
11321
11781
  const headers = await client.buildHeaders();
11322
- const url = `https://backend.leadconnectorhq.com/forms${path13}`;
11782
+ const url = `https://backend.leadconnectorhq.com/forms${path14}`;
11323
11783
  const options = { method, headers };
11324
11784
  if (body && (method === "POST" || method === "PUT")) {
11325
11785
  options.body = JSON.stringify(body);
@@ -11327,7 +11787,7 @@ async function formApiRequest(client, method, path13, body) {
11327
11787
  const response = await fetch(url, options);
11328
11788
  if (!response.ok) {
11329
11789
  const text2 = await response.text();
11330
- throw new Error(`Form API Error ${response.status}: ${method} ${path13}
11790
+ throw new Error(`Form API Error ${response.status}: ${method} ${path14}
11331
11791
  ${text2}`);
11332
11792
  }
11333
11793
  const text = await response.text();
@@ -11341,7 +11801,7 @@ ${text2}`);
11341
11801
  function registerFormBuilderTools(server2, builderClient, publicClient) {
11342
11802
  const client = builderClient;
11343
11803
  if (!client) return;
11344
- const formRequest = (method, path13, body) => formApiRequest(client, method, path13, body);
11804
+ const formRequest = (method, path14, body) => formApiRequest(client, method, path14, body);
11345
11805
  server2.tool(
11346
11806
  "get_form_full",
11347
11807
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns everything the form builder UI shows.",
@@ -11462,10 +11922,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
11462
11922
  },
11463
11923
  async ({ formId, limit, skip }) => {
11464
11924
  try {
11465
- let path13 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
11466
- if (formId) path13 += `&formId=${formId}`;
11467
- if (skip) path13 += `&skip=${skip}`;
11468
- const result = await formRequest("GET", path13);
11925
+ let path14 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
11926
+ if (formId) path14 += `&formId=${formId}`;
11927
+ if (skip) path14 += `&skip=${skip}`;
11928
+ const result = await formRequest("GET", path14);
11469
11929
  return {
11470
11930
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
11471
11931
  };
@@ -11858,9 +12318,9 @@ var import_zod40 = require("zod");
11858
12318
  function registerPipelineBuilderTools(server2, builderClient) {
11859
12319
  const client = builderClient;
11860
12320
  if (!client) return;
11861
- async function pipelineRequest(method, path13, body) {
12321
+ async function pipelineRequest(method, path14, body) {
11862
12322
  const headers = await client.buildHeaders();
11863
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path13}`;
12323
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path14}`;
11864
12324
  const options = { method, headers };
11865
12325
  if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
11866
12326
  options.body = JSON.stringify(body);
@@ -11868,7 +12328,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
11868
12328
  const response = await fetch(url, options);
11869
12329
  if (!response.ok) {
11870
12330
  const text2 = await response.text();
11871
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path13}
12331
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path14}
11872
12332
  ${text2}`);
11873
12333
  }
11874
12334
  const text = await response.text();
@@ -12613,7 +13073,7 @@ ${lines.join("\n")}
12613
13073
  // src/tools/bulk-operations.ts
12614
13074
  var import_zod44 = require("zod");
12615
13075
  function delay(ms) {
12616
- return new Promise((resolve6) => setTimeout(resolve6, ms));
13076
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
12617
13077
  }
12618
13078
  function formatResults(op, results, total) {
12619
13079
  return `${op}: ${results.success} success, ${results.failed} failed out of ${total}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
@@ -12738,7 +13198,7 @@ function registerBulkOperationTools(server2, client) {
12738
13198
  // src/tools/account-export.ts
12739
13199
  var import_zod45 = require("zod");
12740
13200
  function delay2(ms) {
12741
- return new Promise((resolve6) => setTimeout(resolve6, ms));
13201
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
12742
13202
  }
12743
13203
  function registerAccountExportTools(server2, client) {
12744
13204
  const builderClient = WorkflowBuilderClient.fromEnv();
@@ -13059,9 +13519,9 @@ var OBJECT_KEYS = ["contacts", "opportunity"];
13059
13519
  function registerSmartListTools(server2, builderClient) {
13060
13520
  const client = builderClient;
13061
13521
  if (!client) return;
13062
- async function smartListRequest(method, path13, body) {
13522
+ async function smartListRequest(method, path14, body) {
13063
13523
  const headers = await client.buildHeaders();
13064
- const url = `${SMARTLIST_BASE}${path13}`;
13524
+ const url = `${SMARTLIST_BASE}${path14}`;
13065
13525
  const options = { method, headers };
13066
13526
  if (body && (method === "POST" || method === "PUT")) {
13067
13527
  options.body = JSON.stringify(body);
@@ -13069,7 +13529,7 @@ function registerSmartListTools(server2, builderClient) {
13069
13529
  const response = await fetch(url, options);
13070
13530
  if (!response.ok) {
13071
13531
  const text2 = await response.text();
13072
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path13}
13532
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path14}
13073
13533
  ${text2}`);
13074
13534
  }
13075
13535
  const text = await response.text();
@@ -13201,12 +13661,12 @@ var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
13201
13661
  function registerReputationTools(server2, builderClient) {
13202
13662
  const client = builderClient;
13203
13663
  if (!client) return;
13204
- async function reputationRequest(method, path13) {
13664
+ async function reputationRequest(method, path14) {
13205
13665
  const headers = await client.buildHeaders();
13206
- const response = await fetch(`${REPUTATION_BASE}${path13}`, { method, headers });
13666
+ const response = await fetch(`${REPUTATION_BASE}${path14}`, { method, headers });
13207
13667
  if (!response.ok) {
13208
13668
  const text2 = await response.text();
13209
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path13}
13669
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path14}
13210
13670
  ${text2}`);
13211
13671
  }
13212
13672
  const text = await response.text();
@@ -13321,16 +13781,16 @@ var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
13321
13781
  function registerMembershipTools(server2, builderClient) {
13322
13782
  const client = builderClient;
13323
13783
  if (!client) return;
13324
- async function membershipRequest(path13, method = "GET", body) {
13784
+ async function membershipRequest(path14, method = "GET", body) {
13325
13785
  const headers = await client.buildHeaders();
13326
- const response = await fetch(`${MEMBERSHIP_BASE}${path13}`, {
13786
+ const response = await fetch(`${MEMBERSHIP_BASE}${path14}`, {
13327
13787
  method,
13328
13788
  headers,
13329
13789
  body: body ? JSON.stringify(body) : void 0
13330
13790
  });
13331
13791
  if (!response.ok) {
13332
13792
  const text2 = await response.text();
13333
- throw new Error(`Membership API Error ${response.status}: ${method} ${path13}
13793
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path14}
13334
13794
  ${text2}`);
13335
13795
  }
13336
13796
  const text = await response.text();
@@ -13499,7 +13959,7 @@ var import_zod51 = require("zod");
13499
13959
  var fs7 = __toESM(require("fs"));
13500
13960
  var path6 = __toESM(require("path"));
13501
13961
  function delay3(ms) {
13502
- return new Promise((resolve6) => setTimeout(resolve6, ms));
13962
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
13503
13963
  }
13504
13964
  var TemplateSchema = import_zod51.z.object({
13505
13965
  templateName: import_zod51.z.string(),
@@ -13643,7 +14103,7 @@ function registerTemplateDeployerTools(server2, client) {
13643
14103
  const locId = client.resolveLocationId(locationId2);
13644
14104
  const safePath = validateTemplatePath(templateFile);
13645
14105
  const template = TemplateSchema.parse(JSON.parse(fs7.readFileSync(safePath, "utf-8")));
13646
- const resolve6 = (text) => {
14106
+ const resolve7 = (text) => {
13647
14107
  if (typeof text !== "string") return text;
13648
14108
  let result = text;
13649
14109
  for (const [key, value] of Object.entries(answers)) {
@@ -13656,7 +14116,7 @@ function registerTemplateDeployerTools(server2, client) {
13656
14116
  return result;
13657
14117
  };
13658
14118
  const resolveObj = (obj) => {
13659
- if (typeof obj === "string") return resolve6(obj);
14119
+ if (typeof obj === "string") return resolve7(obj);
13660
14120
  if (Array.isArray(obj)) return obj.map(resolveObj);
13661
14121
  if (obj && typeof obj === "object") {
13662
14122
  const result = {};
@@ -14336,7 +14796,35 @@ async function fullWorkflowCatalog(builderClient) {
14336
14796
  return { ids, rows, complete };
14337
14797
  }
14338
14798
  function registerValidatorTools(server2, client, builderClient) {
14339
- if (!builderClient) return;
14799
+ if (!builderClient) {
14800
+ const finishInstallStub = (asked) => ({
14801
+ content: [{
14802
+ type: "text",
14803
+ text: [
14804
+ `\`${asked}\` needs the one-time browser login that finishes your install (step 2 of 2).`,
14805
+ "",
14806
+ 'Say: **"Unlock the Workflow Builder"** (or run `capture_firebase_interactive`).',
14807
+ "A Chrome window opens, you log into GHL once, and the install finishes itself \u2014 about 60 seconds.",
14808
+ "Then restart Claude once, and this tool will be live.",
14809
+ "",
14810
+ "No Chrome on this machine? Run `auto_capture_firebase_script` for the copy-paste path instead."
14811
+ ].join("\n")
14812
+ }]
14813
+ });
14814
+ server2.tool(
14815
+ "validate_workflow",
14816
+ "Pre-flight ID validation for ONE deployed GHL workflow. [NEEDS INSTALL STEP 2 \u2014 run capture_firebase_interactive (one-time browser login) to activate this tool.]",
14817
+ { workflowId: import_zod52.z.string().describe("The workflow ID to validate.") },
14818
+ async () => finishInstallStub("validate_workflow")
14819
+ );
14820
+ server2.tool(
14821
+ "audit_workflows",
14822
+ "Account-wide silent-failure audit of every workflow. [NEEDS INSTALL STEP 2 \u2014 run capture_firebase_interactive (one-time browser login) to activate this tool.]",
14823
+ {},
14824
+ async () => finishInstallStub("audit_workflows")
14825
+ );
14826
+ return;
14827
+ }
14340
14828
  server2.tool(
14341
14829
  "validate_workflow",
14342
14830
  "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'.",
@@ -14521,7 +15009,7 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
14521
15009
  })();
14522
15010
  const firebasePromise = (async () => {
14523
15011
  if (!builderClient) {
14524
- return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The 54 Firebase-gated tools need Firebase credentials. The other 179 tools work fine without. To add it: run enable_workflow_builder with the three Firebase values from your GHL browser session (see elitedcs.com/ghl-mcp-firebase for DevTools steps). Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart. enable_workflow_builder saves and verifies them for you." };
15012
+ return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured \u2014 your install is one step from finished. Run capture_firebase_interactive: a Chrome window opens, you log into GHL once, and it captures + verifies everything itself (about 60 seconds). This powers the account auditor and the Firebase-gated tools. No Chrome on this machine? auto_capture_firebase_script is the copy-paste path. Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart." };
14525
15013
  }
14526
15014
  const result = await builderClient.checkAuth();
14527
15015
  const tokenCompany = builderClient.getTokenCompanyId();
@@ -15317,9 +15805,9 @@ function presetForBusinessType(type) {
15317
15805
  return "generic";
15318
15806
  }
15319
15807
  }
15320
- function setPath(target, path13, value) {
15808
+ function setPath(target, path14, value) {
15321
15809
  if (value === void 0) return;
15322
- const parts = path13.split(".");
15810
+ const parts = path14.split(".");
15323
15811
  let node = target;
15324
15812
  for (let i = 0; i < parts.length - 1; i++) {
15325
15813
  const k = parts[i];
@@ -17405,15 +17893,15 @@ function extractFunnelId(result) {
17405
17893
  return void 0;
17406
17894
  }
17407
17895
  function makeExecuteDeps(client, builderClient, locationId2) {
17408
- const pipelineApi = async (method, path13, body) => {
17896
+ const pipelineApi = async (method, path14, body) => {
17409
17897
  const headers = await builderClient.buildHeaders();
17410
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path13}`;
17898
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path14}`;
17411
17899
  const options = { method, headers };
17412
17900
  if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
17413
17901
  const response = await fetch(url, options);
17414
17902
  if (!response.ok) {
17415
17903
  const text2 = await response.text();
17416
- throw new Error(`Pipeline API ${response.status}: ${method} ${path13}
17904
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path14}
17417
17905
  ${text2.slice(0, 300)}`);
17418
17906
  }
17419
17907
  const text = await response.text();
@@ -17424,17 +17912,17 @@ ${text2.slice(0, 300)}`);
17424
17912
  return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
17425
17913
  }
17426
17914
  };
17427
- const funnelApi = async (method, path13, body) => {
17915
+ const funnelApi = async (method, path14, body) => {
17428
17916
  const headers = await builderClient.buildHeaders();
17429
17917
  headers.Origin = "https://app.gohighlevel.com";
17430
17918
  headers.Referer = "https://app.gohighlevel.com/";
17431
- const url = `https://backend.leadconnectorhq.com/funnels${path13}`;
17919
+ const url = `https://backend.leadconnectorhq.com/funnels${path14}`;
17432
17920
  const options = { method, headers };
17433
17921
  if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
17434
17922
  const response = await fetch(url, options);
17435
17923
  if (!response.ok) {
17436
17924
  const text2 = await response.text();
17437
- throw new Error(`Funnel API ${response.status}: ${method} ${path13}
17925
+ throw new Error(`Funnel API ${response.status}: ${method} ${path14}
17438
17926
  ${text2.slice(0, 300)}`);
17439
17927
  }
17440
17928
  const text = await response.text();
@@ -18351,21 +18839,29 @@ function registerMetaTools(server2, installedVersion) {
18351
18839
  }
18352
18840
 
18353
18841
  // src/cli.ts
18354
- var import_node_util = require("node:util");
18355
- var fs9 = __toESM(require("fs"));
18356
- var path8 = __toESM(require("path"));
18842
+ var import_node_util2 = require("node:util");
18843
+ var fs10 = __toESM(require("fs"));
18844
+ var path9 = __toESM(require("path"));
18357
18845
  var import_crypto2 = require("crypto");
18358
18846
  init_ghl_client();
18359
18847
  init_token_registry();
18360
18848
  init_credentials_store();
18361
18849
  init_setup_tool();
18362
- var EXIT_OK = 0;
18363
- var EXIT_USAGE = 2;
18850
+ var EXIT_OK2 = 0;
18851
+ var EXIT_USAGE2 = 2;
18364
18852
  var EXIT_VALIDATION = 3;
18365
18853
  var EXIT_FS = 4;
18366
18854
  var USAGE = `Usage: ghl-mcp cli <subcommand> [options]
18367
18855
 
18368
18856
  Subcommands:
18857
+ install Add GHL Command to Claude Desktop's settings file
18858
+ (merges safely with existing MCP servers; backs up
18859
+ first; repairs unreadable configs where possible)
18860
+ --dry-run Show what would change without writing anything
18861
+ --print-only Print the merged JSON instead of writing it
18862
+ --path <file> Explicit claude_desktop_config.json location
18863
+ --force Replace a different existing "ghl" entry
18864
+
18369
18865
  register-location Add a sub-account's Private Integration key
18370
18866
  --location-id <id> GHL Location ID (required)
18371
18867
  --api-key <pit-...> The sub-account's Private Integration key (required)
@@ -18396,9 +18892,9 @@ function errLine(msg2) {
18396
18892
  function preflightWritable() {
18397
18893
  try {
18398
18894
  const dir = ensureAppDataDir();
18399
- const probe = path8.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
18400
- fs9.writeFileSync(probe, "ok");
18401
- fs9.unlinkSync(probe);
18895
+ const probe = path9.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
18896
+ fs10.writeFileSync(probe, "ok");
18897
+ fs10.unlinkSync(probe);
18402
18898
  return true;
18403
18899
  } catch (error) {
18404
18900
  errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
@@ -18422,7 +18918,7 @@ function confirmSaved(registry2) {
18422
18918
  function parse(argv, options, required) {
18423
18919
  let parsed;
18424
18920
  try {
18425
- parsed = (0, import_node_util.parseArgs)({ args: argv, options, strict: true, allowPositionals: false });
18921
+ parsed = (0, import_node_util2.parseArgs)({ args: argv, options, strict: true, allowPositionals: false });
18426
18922
  } catch (error) {
18427
18923
  return { usageError: error instanceof Error ? error.message : String(error) };
18428
18924
  }
@@ -18449,7 +18945,7 @@ async function cmdRegisterLocation(argv, registry2) {
18449
18945
  if ("usageError" in p) {
18450
18946
  errLine(p.usageError);
18451
18947
  errLine(USAGE);
18452
- return EXIT_USAGE;
18948
+ return EXIT_USAGE2;
18453
18949
  }
18454
18950
  const locationId2 = p.values["location-id"].trim();
18455
18951
  const apiKey2 = p.values["api-key"].trim();
@@ -18482,7 +18978,7 @@ async function cmdRegisterLocation(argv, registry2) {
18482
18978
  `
18483
18979
  );
18484
18980
  restartReminder();
18485
- return EXIT_OK;
18981
+ return EXIT_OK2;
18486
18982
  }
18487
18983
  async function cmdRegisterCompanyFirebase(argv, registry2) {
18488
18984
  const p = parse(
@@ -18500,7 +18996,7 @@ async function cmdRegisterCompanyFirebase(argv, registry2) {
18500
18996
  if ("usageError" in p) {
18501
18997
  errLine(p.usageError);
18502
18998
  errLine(USAGE);
18503
- return EXIT_USAGE;
18999
+ return EXIT_USAGE2;
18504
19000
  }
18505
19001
  const typedCompanyId = p.values["company-id"].trim();
18506
19002
  const refreshToken = p.values["refresh-token"].trim();
@@ -18510,7 +19006,7 @@ async function cmdRegisterCompanyFirebase(argv, registry2) {
18510
19006
  if (!apiKey2) {
18511
19007
  errLine("No Firebase API key available. Pass --api-key (starts with 'AIza'), or seed the home");
18512
19008
  errLine("Firebase first (the key is identical across GHL accounts).");
18513
- return EXIT_USAGE;
19009
+ return EXIT_USAGE2;
18514
19010
  }
18515
19011
  let canonicalCompanyId = typedCompanyId;
18516
19012
  if (!p.values["no-validate"]) {
@@ -18549,7 +19045,7 @@ async function cmdRegisterCompanyFirebase(argv, registry2) {
18549
19045
  `
18550
19046
  );
18551
19047
  restartReminder();
18552
- return EXIT_OK;
19048
+ return EXIT_OK2;
18553
19049
  }
18554
19050
  async function cmdRegisterAgencyKey(argv, registry2) {
18555
19051
  const p = parse(
@@ -18560,7 +19056,7 @@ async function cmdRegisterAgencyKey(argv, registry2) {
18560
19056
  if ("usageError" in p) {
18561
19057
  errLine(p.usageError);
18562
19058
  errLine(USAGE);
18563
- return EXIT_USAGE;
19059
+ return EXIT_USAGE2;
18564
19060
  }
18565
19061
  const apiKey2 = p.values["api-key"].trim();
18566
19062
  if (!p.values["no-validate"]) {
@@ -18589,7 +19085,7 @@ async function cmdRegisterAgencyKey(argv, registry2) {
18589
19085
  process.stdout.write(`Registered agency key: ${apiKey2.substring(0, 12)}... \u2192 ${tokenRegistryPath()}
18590
19086
  `);
18591
19087
  restartReminder();
18592
- return EXIT_OK;
19088
+ return EXIT_OK2;
18593
19089
  }
18594
19090
  function cmdListLocations(registry2) {
18595
19091
  const locs = registry2.listLocations().map((loc) => {
@@ -18606,15 +19102,19 @@ function cmdListLocations(registry2) {
18606
19102
  companyFirebases: companies.map(({ companyId, name }) => ({ companyId, ...name ? { name } : {} }))
18607
19103
  };
18608
19104
  process.stdout.write(JSON.stringify(out, null, 2) + "\n");
18609
- return EXIT_OK;
19105
+ return EXIT_OK2;
18610
19106
  }
18611
19107
  async function runCli(subcommand, argv) {
19108
+ if (subcommand === "install") {
19109
+ const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_config_installer(), config_installer_exports));
19110
+ return runInstall2(argv);
19111
+ }
18612
19112
  let registry2;
18613
19113
  try {
18614
19114
  registry2 = new TokenRegistry();
18615
19115
  } catch (error) {
18616
19116
  errLine(error instanceof Error ? error.message : String(error));
18617
- return EXIT_USAGE;
19117
+ return EXIT_USAGE2;
18618
19118
  }
18619
19119
  const loadFailure = registry2.getLoadFailure();
18620
19120
  if (loadFailure) {
@@ -18635,11 +19135,11 @@ async function runCli(subcommand, argv) {
18635
19135
  case "--help":
18636
19136
  case "-h":
18637
19137
  process.stdout.write(USAGE + "\n");
18638
- return subcommand === void 0 ? EXIT_USAGE : EXIT_OK;
19138
+ return subcommand === void 0 ? EXIT_USAGE2 : EXIT_OK2;
18639
19139
  default:
18640
19140
  errLine(`Unknown subcommand: ${subcommand}`);
18641
19141
  errLine(USAGE);
18642
- return EXIT_USAGE;
19142
+ return EXIT_USAGE2;
18643
19143
  }
18644
19144
  }
18645
19145
 
@@ -18648,7 +19148,7 @@ var bundledPkg = require_package();
18648
19148
  var pkg = (() => {
18649
19149
  try {
18650
19150
  const onDisk = JSON.parse(
18651
- fs13.readFileSync(path12.resolve(__dirname, "..", "package.json"), "utf8")
19151
+ fs14.readFileSync(path13.resolve(__dirname, "..", "package.json"), "utf8")
18652
19152
  );
18653
19153
  if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
18654
19154
  return { version: onDisk.version };
@@ -18661,7 +19161,7 @@ dotenv2.config();
18661
19161
  setPkgVersion(pkg.version);
18662
19162
  {
18663
19163
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
18664
- if (configDirOverride && !path12.isAbsolute(configDirOverride)) {
19164
+ if (configDirOverride && !path13.isAbsolute(configDirOverride)) {
18665
19165
  process.stderr.write(
18666
19166
  `[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.
18667
19167
  `
@@ -18674,20 +19174,20 @@ process.on("unhandledRejection", (reason) => {
18674
19174
  `);
18675
19175
  });
18676
19176
  function hardenSecretFilePerms() {
18677
- const repoDir = path12.resolve(__dirname, "..");
19177
+ const repoDir = path13.resolve(__dirname, "..");
18678
19178
  const candidates = [
18679
- { file: path12.join(repoDir, "start-mcp.sh"), mode: 448 },
19179
+ { file: path13.join(repoDir, "start-mcp.sh"), mode: 448 },
18680
19180
  // Legacy registry location (pre-migration); new location lives in app-data.
18681
- { file: path12.join(repoDir, ".ghl-tokens.json"), mode: 384 },
19181
+ { file: path13.join(repoDir, ".ghl-tokens.json"), mode: 384 },
18682
19182
  { file: tokenRegistryPath(), mode: 384 }
18683
19183
  ];
18684
19184
  for (const { file, mode } of candidates) {
18685
19185
  let current;
18686
19186
  try {
18687
- if (!fs13.existsSync(file)) continue;
18688
- current = fs13.statSync(file).mode & 511;
19187
+ if (!fs14.existsSync(file)) continue;
19188
+ current = fs14.statSync(file).mode & 511;
18689
19189
  if (current !== mode) {
18690
- fs13.chmodSync(file, mode);
19190
+ fs14.chmodSync(file, mode);
18691
19191
  }
18692
19192
  } catch (error) {
18693
19193
  const message = error instanceof Error ? error.message : String(error);
@@ -18878,7 +19378,10 @@ async function resolveAccessAndRegister() {
18878
19378
  registerSetupTool(server, pkg.version);
18879
19379
  registerLeadCaptureTool(server);
18880
19380
  registerOutOfBand(
18881
- [{ name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) }],
19381
+ [
19382
+ { name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) },
19383
+ { name: "capture_firebase_interactive", register: () => registerInteractiveCaptureTool(server) }
19384
+ ],
18882
19385
  gatingConfig,
18883
19386
  outOfBandTracker
18884
19387
  );
@@ -18892,6 +19395,13 @@ async function resolveAccessAndRegister() {
18892
19395
  // a fresh refresh token when it rotates. Registered on the FREE tier too:
18893
19396
  // the Firebase login powers the read-only auditor tools (audit_workflows,
18894
19397
  // validate_workflow, *_full reads) — only the write tools stay gated.
19398
+ // v3.60.0: setup_ghl_mcp registers in NORMAL mode too — it is the
19399
+ // ONE-LINE UPGRADE path ("email + new license key, nothing else").
19400
+ // Before this it existed only in bootstrap, so an installed free user
19401
+ // pasting the upgrade line got "Tool setup_ghl_mcp not found" — the
19402
+ // exact audience the upgrade exists for. Caught by the live wire
19403
+ // proof, not by unit tests (the handler worked; registration didn't).
19404
+ { name: "setup_ghl_mcp", register: () => registerSetupTool(server, pkg.version) },
18895
19405
  { name: "enable_workflow_builder", register: () => registerEnableWorkflowBuilderTool(server) },
18896
19406
  { name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) },
18897
19407
  // One-click Builder unlock — buyer just logs into a Chrome window the