@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/CHANGELOG.md +65 -0
- package/README.md +23 -10
- package/dist/index.js +701 -191
- package/package.json +3 -2
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(
|
|
118
|
-
const url = new 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,
|
|
129
|
-
const url = this.buildUrl(
|
|
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} ${
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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(
|
|
187
|
-
return this.request("GET",
|
|
186
|
+
async get(path14, options) {
|
|
187
|
+
return this.request("GET", path14, options);
|
|
188
188
|
}
|
|
189
|
-
async post(
|
|
190
|
-
return this.request("POST",
|
|
189
|
+
async post(path14, options) {
|
|
190
|
+
return this.request("POST", path14, options);
|
|
191
191
|
}
|
|
192
|
-
async put(
|
|
193
|
-
return this.request("PUT",
|
|
192
|
+
async put(path14, options) {
|
|
193
|
+
return this.request("PUT", path14, options);
|
|
194
194
|
}
|
|
195
|
-
async patch(
|
|
196
|
-
return this.request("PATCH",
|
|
195
|
+
async patch(path14, options) {
|
|
196
|
+
return this.request("PATCH", path14, options);
|
|
197
197
|
}
|
|
198
|
-
async delete(
|
|
199
|
-
return this.request("DELETE",
|
|
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
|
|
1273
|
+
const os7 = require("node:os");
|
|
1274
1274
|
const crypto5 = require("node:crypto");
|
|
1275
|
-
const raw = `${
|
|
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
|
|
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
|
-
|
|
1516
|
-
|
|
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://
|
|
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
|
|
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})
|
|
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:
|
|
1586
|
-
ghl_location_id:
|
|
1587
|
-
ghl_company_id: args.ghl_company_id?.trim() || void 0,
|
|
1588
|
-
ghl_user_id:
|
|
1589
|
-
ghl_firebase_api_key:
|
|
1590
|
-
ghl_firebase_refresh_token:
|
|
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
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
2956
|
+
fs11.accessSync(systemApps, fs11.constants.W_OK);
|
|
2497
2957
|
base = "";
|
|
2498
2958
|
} catch {
|
|
2499
2959
|
}
|
|
2500
|
-
const appDir = base ?
|
|
2501
|
-
const macOSDir =
|
|
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:
|
|
2531
|
-
{ path:
|
|
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 =
|
|
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 =
|
|
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,
|
|
3033
|
+
const plan = planLauncher(process.platform, os5.homedir(), port, installingEntry());
|
|
2574
3034
|
try {
|
|
2575
3035
|
for (const f of plan.files) {
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
|
3057
|
+
var fs11, os5, path10;
|
|
2598
3058
|
var init_launcher = __esm({
|
|
2599
3059
|
"src/launcher.ts"() {
|
|
2600
3060
|
"use strict";
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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 =
|
|
2633
|
-
const fromPath = (process.env.PATH || "").split(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3211
|
+
resolve7(outcome);
|
|
2752
3212
|
});
|
|
2753
3213
|
});
|
|
2754
3214
|
}
|
|
2755
|
-
var import_child_process2,
|
|
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
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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
|
|
3942
|
+
return path12.join(appDataDir(), "intake-overlay.json");
|
|
3483
3943
|
}
|
|
3484
3944
|
function readOverlay() {
|
|
3485
3945
|
try {
|
|
3486
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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
|
|
3966
|
+
return path12.join(appDataDir(), "cockpit-state.json");
|
|
3507
3967
|
}
|
|
3508
3968
|
function readCockpitState() {
|
|
3509
3969
|
try {
|
|
3510
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
4318
|
-
ls.on("error", () =>
|
|
4777
|
+
ls.on("close", () => resolve7(recorderAllowPatterns(buf)));
|
|
4778
|
+
ls.on("error", () => resolve7([]));
|
|
4319
4779
|
setTimeout(() => {
|
|
4320
4780
|
ls.kill("SIGKILL");
|
|
4321
|
-
|
|
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((
|
|
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", () =>
|
|
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((
|
|
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", () =>
|
|
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((
|
|
5111
|
+
await new Promise((resolve7, reject) => {
|
|
4652
5112
|
server2.once("error", reject);
|
|
4653
|
-
server2.listen(port, "127.0.0.1",
|
|
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((
|
|
4680
|
-
const stop = () => server2.close(() =>
|
|
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
|
|
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
|
-
|
|
4690
|
-
|
|
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
|
|
4726
|
-
var
|
|
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,
|
|
9221
|
+
async function builderRequest(method, path14, body) {
|
|
8762
9222
|
const headers = await client.buildHeaders();
|
|
8763
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
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${
|
|
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(
|
|
10028
|
-
return client.request("GET",
|
|
10487
|
+
async function internalGet(path14) {
|
|
10488
|
+
return client.request("GET", path14);
|
|
10029
10489
|
}
|
|
10030
|
-
async function internalPost(
|
|
10031
|
-
return client.request("POST",
|
|
10490
|
+
async function internalPost(path14, body) {
|
|
10491
|
+
return client.request("POST", path14, body);
|
|
10032
10492
|
}
|
|
10033
|
-
async function internalPut(
|
|
10034
|
-
return client.request("PUT",
|
|
10493
|
+
async function internalPut(path14, body) {
|
|
10494
|
+
return client.request("PUT", path14, body);
|
|
10035
10495
|
}
|
|
10036
|
-
async function internalDelete(
|
|
10037
|
-
return client.request("DELETE",
|
|
10496
|
+
async function internalDelete(path14) {
|
|
10497
|
+
return client.request("DELETE", path14);
|
|
10038
10498
|
}
|
|
10039
|
-
async function funnelRequest(method,
|
|
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${
|
|
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} ${
|
|
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,
|
|
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${
|
|
11001
|
-
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
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,
|
|
11780
|
+
async function formApiRequest(client, method, path14, body) {
|
|
11321
11781
|
const headers = await client.buildHeaders();
|
|
11322
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
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} ${
|
|
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,
|
|
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
|
|
11466
|
-
if (formId)
|
|
11467
|
-
if (skip)
|
|
11468
|
-
const result = await formRequest("GET",
|
|
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,
|
|
12321
|
+
async function pipelineRequest(method, path14, body) {
|
|
11862
12322
|
const headers = await client.buildHeaders();
|
|
11863
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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((
|
|
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((
|
|
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,
|
|
13522
|
+
async function smartListRequest(method, path14, body) {
|
|
13063
13523
|
const headers = await client.buildHeaders();
|
|
13064
|
-
const url = `${SMARTLIST_BASE}${
|
|
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} ${
|
|
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,
|
|
13664
|
+
async function reputationRequest(method, path14) {
|
|
13205
13665
|
const headers = await client.buildHeaders();
|
|
13206
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
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} ${
|
|
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(
|
|
13784
|
+
async function membershipRequest(path14, method = "GET", body) {
|
|
13325
13785
|
const headers = await client.buildHeaders();
|
|
13326
|
-
const response = await fetch(`${MEMBERSHIP_BASE}${
|
|
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} ${
|
|
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((
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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,
|
|
15808
|
+
function setPath(target, path14, value) {
|
|
15321
15809
|
if (value === void 0) return;
|
|
15322
|
-
const parts =
|
|
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,
|
|
17896
|
+
const pipelineApi = async (method, path14, body) => {
|
|
17409
17897
|
const headers = await builderClient.buildHeaders();
|
|
17410
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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,
|
|
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${
|
|
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} ${
|
|
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
|
|
18355
|
-
var
|
|
18356
|
-
var
|
|
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
|
|
18363
|
-
var
|
|
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 =
|
|
18400
|
-
|
|
18401
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
19138
|
+
return subcommand === void 0 ? EXIT_USAGE2 : EXIT_OK2;
|
|
18639
19139
|
default:
|
|
18640
19140
|
errLine(`Unknown subcommand: ${subcommand}`);
|
|
18641
19141
|
errLine(USAGE);
|
|
18642
|
-
return
|
|
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
|
-
|
|
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 && !
|
|
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 =
|
|
19177
|
+
const repoDir = path13.resolve(__dirname, "..");
|
|
18678
19178
|
const candidates = [
|
|
18679
|
-
{ file:
|
|
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:
|
|
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 (!
|
|
18688
|
-
current =
|
|
19187
|
+
if (!fs14.existsSync(file)) continue;
|
|
19188
|
+
current = fs14.statSync(file).mode & 511;
|
|
18689
19189
|
if (current !== mode) {
|
|
18690
|
-
|
|
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
|
-
[
|
|
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
|