@elitedcs/ghl-mcp 3.67.0 → 3.68.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
@@ -1648,7 +1648,7 @@ Note: Firebase credentials rejected (${fb.error}).`;
1648
1648
  const telemetryLine = telemetryDisabled(process.env) ? "" : `
1649
1649
 
1650
1650
  ${TELEMETRY_DISCLOSURE}`;
1651
- const finishedCount = isFree ? "111" : "241";
1651
+ const finishedCount = isFree ? "112" : "242";
1652
1652
  const freeTip = isFree ? `
1653
1653
 
1654
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` : "";
@@ -1693,7 +1693,7 @@ Free tier: read-only. Write tools stay visible but answer with upgrade info inst
1693
1693
  function registerEnableWorkflowBuilderTool(server2) {
1694
1694
  server2.tool(
1695
1695
  "enable_workflow_builder",
1696
- "Add Firebase credentials to an existing GHL Command install to unlock 55 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 186 to 241 after the next Claude restart.",
1696
+ "Add Firebase credentials to an existing GHL Command install to unlock 55 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 187 to 242 after the next Claude restart.",
1697
1697
  {
1698
1698
  // v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
1699
1699
  // get the console script; the script returns a JSON object that pastes
@@ -2854,9 +2854,9 @@ var require_package = __commonJS({
2854
2854
  "package.json"(exports2, module2) {
2855
2855
  module2.exports = {
2856
2856
  name: "@elitedcs/ghl-mcp",
2857
- version: "3.67.0",
2857
+ version: "3.68.0",
2858
2858
  mcpName: "io.github.drjerryrelth/ghl-command",
2859
- description: "GoHighLevel MCP Server for Claude. 241 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.",
2859
+ description: "GoHighLevel MCP Server for Claude. 242 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.",
2860
2860
  main: "dist/index.js",
2861
2861
  bin: {
2862
2862
  "ghl-mcp": "dist/index.js"
@@ -6506,8 +6506,10 @@ var FREE_TIER_EXTRA_READ_TOOLS = /* @__PURE__ */ new Set([
6506
6506
  // live funnel QA (reads + public URL probes)
6507
6507
  "check_blog_slug",
6508
6508
  // slug availability lookup
6509
- "workflow_builder_status"
6509
+ "workflow_builder_status",
6510
6510
  // no-Firebase fallback diagnostic (pure status read)
6511
+ "run_checkup"
6512
+ // whole-install checkup: install layer + live capability probe (all reads)
6511
6513
  ]);
6512
6514
  var FREE_TIER_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
6513
6515
  "export_account"
@@ -10350,10 +10352,10 @@ function registerWebhookTools(server2, builderClient) {
10350
10352
  confirm: import_zod34.z.literal("DELETE").describe("Must be 'DELETE' to confirm this destructive action."),
10351
10353
  force: import_zod34.z.boolean().optional().describe("Delete even when the workflow contains other actions that would go with it.")
10352
10354
  },
10353
- async ({ webhookId, confirm, force }) => {
10355
+ async ({ webhookId, confirm: confirm2, force }) => {
10354
10356
  try {
10355
10357
  const client = requireClient();
10356
- if (confirm !== "DELETE") throw new Error("Pass confirm: 'DELETE' to delete a webhook.");
10358
+ if (confirm2 !== "DELETE") throw new Error("Pass confirm: 'DELETE' to delete a webhook.");
10357
10359
  const summary = summarizeWebhookWorkflow(await client.getWorkflow(webhookId));
10358
10360
  if (!summary) {
10359
10361
  throw new Error(
@@ -12072,7 +12074,7 @@ ${await response.text()}`);
12072
12074
  );
12073
12075
  };
12074
12076
  if (priorDataUrl) refuseCreate(livePageVersion);
12075
- for (let confirm = 0; confirm < 3; confirm++) {
12077
+ for (let confirm2 = 0; confirm2 < 3; confirm2++) {
12076
12078
  await new Promise((r) => setTimeout(r, 1500));
12077
12079
  let again = null;
12078
12080
  try {
@@ -13666,8 +13668,8 @@ function registerBulkOperationTools(server2, client) {
13666
13668
  contactIds: import_zod45.z.array(import_zod45.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
13667
13669
  confirm: import_zod45.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
13668
13670
  },
13669
- async ({ contactIds, confirm }) => {
13670
- if (confirm !== "DELETE") {
13671
+ async ({ contactIds, confirm: confirm2 }) => {
13672
+ if (confirm2 !== "DELETE") {
13671
13673
  return errorResponse("Safety check failed. Pass confirm: 'DELETE' to proceed.");
13672
13674
  }
13673
13675
  const results = { success: 0, failed: 0, errors: [] };
@@ -16249,167 +16251,507 @@ async function getVersionStatus(installed) {
16249
16251
  }
16250
16252
 
16251
16253
  // src/tools/diagnostics.ts
16254
+ async function collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport) {
16255
+ const checks = [];
16256
+ const versionPromise = getVersionStatus(installedVersion);
16257
+ const apiKeyPromise = (async () => {
16258
+ const locId = client.defaultLocationId;
16259
+ if (!locId) {
16260
+ return { name: "GHL API key", status: "warn", detail: `Can't validate without a default location ID. Set GHL_LOCATION_ID or run setup_ghl_mcp.` };
16261
+ }
16262
+ try {
16263
+ const response = await fetch(`https://services.leadconnectorhq.com/locations/${locId}`, {
16264
+ method: "GET",
16265
+ headers: {
16266
+ Authorization: `Bearer ${client.getApiKey()}`,
16267
+ "Content-Type": "application/json",
16268
+ Accept: "application/json",
16269
+ Version: "2021-07-28"
16270
+ },
16271
+ signal: AbortSignal.timeout(8e3)
16272
+ });
16273
+ if (response.status === 401) {
16274
+ return { name: "GHL API key", status: "fail", detail: `401 Unauthorized \u2014 key ${client.getApiKeyPrefix()} is invalid or revoked. Run setup_ghl_mcp with a fresh key.` };
16275
+ }
16276
+ if (response.status === 403) {
16277
+ return { name: "GHL API key", status: "fail", detail: `403 Forbidden \u2014 key ${client.getApiKeyPrefix()} doesn't have access to location ${locId}. The key may belong to a different sub-account.` };
16278
+ }
16279
+ if (!response.ok) {
16280
+ return { name: "GHL API key", status: "warn", detail: `HTTP ${response.status} reading /locations/${locId} \u2014 key may be limited or GHL is having issues.` };
16281
+ }
16282
+ return { name: "GHL API key", status: "pass", detail: `Valid (key ${client.getApiKeyPrefix()}).` };
16283
+ } catch (error) {
16284
+ return { name: "GHL API key", status: "warn", detail: `Network error: ${error instanceof Error ? error.message : String(error)}. Server starts anyway; tool calls will report errors when invoked.` };
16285
+ }
16286
+ })();
16287
+ const locationPromise = (async () => {
16288
+ const locId = client.defaultLocationId;
16289
+ if (!locId) {
16290
+ return { name: "Default location", status: "warn", detail: "No default location set. Tools that don't accept locationId arg will fail." };
16291
+ }
16292
+ try {
16293
+ const result = await client.get(`/locations/${locId}`);
16294
+ const loc = result.location ?? result;
16295
+ return { name: "Default location", status: "pass", detail: `${loc?.name ?? "Unknown"} (${locId}) reachable.` };
16296
+ } catch (error) {
16297
+ return { name: "Default location", status: "fail", detail: `Could not read location ${locId}: ${error instanceof Error ? error.message : String(error)}` };
16298
+ }
16299
+ })();
16300
+ const firebasePromise = (async () => {
16301
+ if (!builderClient) {
16302
+ 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." };
16303
+ }
16304
+ const result = await builderClient.checkAuth();
16305
+ const tokenCompany = builderClient.getTokenCompanyId();
16306
+ const intendedCompany = builderClient.getIntendedCompanyId() ?? builderClient.getCurrentCompanyId();
16307
+ const companyNote = intendedCompany ? ` Active location's company: ${intendedCompany}.` : "";
16308
+ if (result.ok && intendedCompany && tokenCompany && intendedCompany !== tokenCompany) {
16309
+ return {
16310
+ name: "Firebase auth (workflow builder)",
16311
+ status: "fail",
16312
+ detail: `Company mismatch: the active location belongs to company ${intendedCompany}, but the Firebase token authenticates as ${tokenCompany}. Workflow-builder writes will 401 or hit the wrong account. This company's Firebase is missing or registered under the wrong ID. Run register_company_firebase with a token captured from that sub-account's GHL session (v3.27.0+ stores it under the correct internal ID automatically).`
16313
+ };
16314
+ }
16315
+ if (result.ok) {
16316
+ const idNote = tokenCompany ? ` Authenticated as company ${tokenCompany}.` : "";
16317
+ return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}${idNote}` };
16318
+ }
16319
+ return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.${companyNote} If you're in a client's account, register its Firebase with register_company_firebase. Otherwise re-capture your Firebase values from GHL DevTools and re-run enable_workflow_builder.` };
16320
+ })();
16321
+ const registryCheck = (() => {
16322
+ if (!registry2) {
16323
+ return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
16324
+ }
16325
+ const loadFailure = registry2.getLoadFailure();
16326
+ if (loadFailure) {
16327
+ return { name: "Token registry", status: "fail", detail: loadFailure };
16328
+ }
16329
+ const locs = registry2.listLocations();
16330
+ const companies = registry2.listCompanyFirebases();
16331
+ const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
16332
+ if (locs.length === 0) {
16333
+ return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
16334
+ }
16335
+ return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
16336
+ })();
16337
+ const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
16338
+ versionPromise,
16339
+ apiKeyPromise,
16340
+ locationPromise,
16341
+ firebasePromise
16342
+ ]);
16343
+ let versionCheck;
16344
+ if (versionStatus.latest === null) {
16345
+ versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed}. Could not reach npm registry to check for updates: ${versionStatus.fetchError ?? "unknown"}.` };
16346
+ } else if (versionStatus.upToDate) {
16347
+ versionCheck = { name: "npm registry + version", status: "pass", detail: `Up to date at v${versionStatus.installed}.` };
16348
+ } else {
16349
+ versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed} but latest is v${versionStatus.latest}. Quit Claude (Cmd+Q) and reopen to upgrade.` };
16350
+ }
16351
+ const gatingCheck = (() => {
16352
+ const r = getGatingReport?.();
16353
+ if (!r) {
16354
+ return { name: "Tool gating", status: "skip", detail: "Gating report unavailable in this mode." };
16355
+ }
16356
+ if (!r.gatingActive) {
16357
+ return { name: "Tool gating", status: "pass", detail: `No gating configured \u2014 all ${r.registeredCount} tools registered. Sub-account create/delete stays OFF unless GHL_ENABLE_ACCOUNT_ADMIN=1 is set.` };
16358
+ }
16359
+ const parts = [`${r.registeredCount} of ${r.attemptedCount} tools registered.`];
16360
+ if (r.disabledTools.length > 0) parts.push(`Disabled tools: ${r.disabledTools.join(", ")}.`);
16361
+ if (r.disabledModules.length > 0) parts.push(`Disabled modules: ${r.disabledModules.join(", ")}.`);
16362
+ if (r.outOfBandDenied.length > 0) parts.push(`Confirmed unregistered (setup/helper tools): ${r.outOfBandDenied.join(", ")}.`);
16363
+ if (r.allowlistActive) parts.push(`Allowlist: modules=[${r.enabledModules.join(",") || "-"}], tools=[${r.enabledTools.join(",") || "-"}].`);
16364
+ parts.push(`Account admin (sub-account create/delete): ${r.accountAdminEnabled ? "ENABLED via GHL_ENABLE_ACCOUNT_ADMIN=1" : "OFF (default)"}.`);
16365
+ const problems = [];
16366
+ if (r.ignoredHardCoreDenials.length > 0) problems.push(`recovery-core tool(s) in GHL_DISABLED_TOOLS cannot be disabled and were IGNORED: ${r.ignoredHardCoreDenials.join(", ")}`);
16367
+ if (r.unknownDisabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_TOOLS (possible typo \u2014 those tools are NOT disabled): ${r.unknownDisabledTools.join(", ")}`);
16368
+ if (r.unknownDisabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_MODULES (possible typo): ${r.unknownDisabledModules.join(", ")}`);
16369
+ if (r.unknownEnabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_TOOLS: ${r.unknownEnabledTools.join(", ")}`);
16370
+ if (r.unknownEnabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_MODULES: ${r.unknownEnabledModules.join(", ")}`);
16371
+ if (problems.length > 0) {
16372
+ return { name: "Tool gating", status: "warn", detail: `${parts.join(" ")} PROBLEMS: ${problems.join("; ")}. Fix the env var in your Claude config and fully restart Claude.` };
16373
+ }
16374
+ return { name: "Tool gating", status: "pass", detail: parts.join(" ") };
16375
+ })();
16376
+ const telemetryCheck = telemetryDisabled(process.env) ? {
16377
+ name: "Anonymous usage stats",
16378
+ status: "skip",
16379
+ detail: `OFF \u2014 ${process.env.GHL_TELEMETRY?.trim() === "0" ? "GHL_TELEMETRY=0" : "DO_NOT_TRACK"} is set. Nothing is sent.`
16380
+ } : {
16381
+ name: "Anonymous usage stats",
16382
+ status: "pass",
16383
+ detail: `ON. ${TELEMETRY_DISCLOSURE} Sent: install/startup events, package version, Node major, platform, and a random device id. NEVER sent: your license key, email, GHL API key, location ID, hostname, file paths, or any GHL account data.`
16384
+ };
16385
+ checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck, gatingCheck, telemetryCheck);
16386
+ return checks;
16387
+ }
16388
+ function renderChecks(checks) {
16389
+ const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
16390
+ const lines = [];
16391
+ for (const c of checks) {
16392
+ lines.push(`${symbols[c.status]} ${c.name.padEnd(35)} ${c.status.toUpperCase()}`);
16393
+ lines.push(` ${c.detail}`);
16394
+ }
16395
+ return lines;
16396
+ }
16397
+ function summarizeChecks(checks) {
16398
+ const fails = checks.filter((c) => c.status === "fail").length;
16399
+ const warns = checks.filter((c) => c.status === "warn").length;
16400
+ if (fails === 0 && warns === 0) return "All systems go.";
16401
+ const parts = [];
16402
+ if (fails > 0) parts.push(`${fails} failure(s)`);
16403
+ if (warns > 0) parts.push(`${warns} warning(s)`);
16404
+ return parts.join(", ") + " \u2014 see details above.";
16405
+ }
16252
16406
  function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
16253
16407
  server2.tool(
16254
16408
  "health_check",
16255
- "Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), token registry, and the tool-gating policy (disabled tools/modules, account-admin opt-in, typos in gating env vars). Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change.",
16409
+ "Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), token registry, and the tool-gating policy (disabled tools/modules, account-admin opt-in, typos in gating env vars). Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change. For a wider report that also probes every GHL capability live and shows what you are not yet using, run `run_checkup`.",
16256
16410
  {},
16257
16411
  async () => {
16258
- const checks = [];
16259
- const versionPromise = getVersionStatus(installedVersion);
16260
- const apiKeyPromise = (async () => {
16261
- const locId = client.defaultLocationId;
16262
- if (!locId) {
16263
- return { name: "GHL API key", status: "warn", detail: `Can't validate without a default location ID. Set GHL_LOCATION_ID or run setup_ghl_mcp.` };
16264
- }
16265
- try {
16266
- const response = await fetch(`https://services.leadconnectorhq.com/locations/${locId}`, {
16267
- method: "GET",
16268
- headers: {
16269
- Authorization: `Bearer ${client.getApiKey()}`,
16270
- "Content-Type": "application/json",
16271
- Accept: "application/json",
16272
- Version: "2021-07-28"
16273
- },
16274
- signal: AbortSignal.timeout(8e3)
16275
- });
16276
- if (response.status === 401) {
16277
- return { name: "GHL API key", status: "fail", detail: `401 Unauthorized \u2014 key ${client.getApiKeyPrefix()} is invalid or revoked. Run setup_ghl_mcp with a fresh key.` };
16278
- }
16279
- if (response.status === 403) {
16280
- return { name: "GHL API key", status: "fail", detail: `403 Forbidden \u2014 key ${client.getApiKeyPrefix()} doesn't have access to location ${locId}. The key may belong to a different sub-account.` };
16281
- }
16282
- if (!response.ok) {
16283
- return { name: "GHL API key", status: "warn", detail: `HTTP ${response.status} reading /locations/${locId} \u2014 key may be limited or GHL is having issues.` };
16284
- }
16285
- return { name: "GHL API key", status: "pass", detail: `Valid (key ${client.getApiKeyPrefix()}).` };
16286
- } catch (error) {
16287
- return { name: "GHL API key", status: "warn", detail: `Network error: ${error instanceof Error ? error.message : String(error)}. Server starts anyway; tool calls will report errors when invoked.` };
16288
- }
16289
- })();
16290
- const locationPromise = (async () => {
16291
- const locId = client.defaultLocationId;
16292
- if (!locId) {
16293
- return { name: "Default location", status: "warn", detail: "No default location set. Tools that don't accept locationId arg will fail." };
16294
- }
16295
- try {
16296
- const result = await client.get(`/locations/${locId}`);
16297
- const loc = result.location ?? result;
16298
- return { name: "Default location", status: "pass", detail: `${loc?.name ?? "Unknown"} (${locId}) reachable.` };
16299
- } catch (error) {
16300
- return { name: "Default location", status: "fail", detail: `Could not read location ${locId}: ${error instanceof Error ? error.message : String(error)}` };
16301
- }
16302
- })();
16303
- const firebasePromise = (async () => {
16304
- if (!builderClient) {
16305
- 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." };
16306
- }
16307
- const result = await builderClient.checkAuth();
16308
- const tokenCompany = builderClient.getTokenCompanyId();
16309
- const intendedCompany = builderClient.getIntendedCompanyId() ?? builderClient.getCurrentCompanyId();
16310
- const companyNote = intendedCompany ? ` Active location's company: ${intendedCompany}.` : "";
16311
- if (result.ok && intendedCompany && tokenCompany && intendedCompany !== tokenCompany) {
16312
- return {
16313
- name: "Firebase auth (workflow builder)",
16314
- status: "fail",
16315
- detail: `Company mismatch: the active location belongs to company ${intendedCompany}, but the Firebase token authenticates as ${tokenCompany}. Workflow-builder writes will 401 or hit the wrong account. This company's Firebase is missing or registered under the wrong ID. Run register_company_firebase with a token captured from that sub-account's GHL session (v3.27.0+ stores it under the correct internal ID automatically).`
16316
- };
16317
- }
16318
- if (result.ok) {
16319
- const idNote = tokenCompany ? ` Authenticated as company ${tokenCompany}.` : "";
16320
- return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}${idNote}` };
16321
- }
16322
- return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.${companyNote} If you're in a client's account, register its Firebase with register_company_firebase. Otherwise re-capture your Firebase values from GHL DevTools and re-run enable_workflow_builder.` };
16323
- })();
16324
- const registryCheck = (() => {
16325
- if (!registry2) {
16326
- return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
16327
- }
16328
- const loadFailure = registry2.getLoadFailure();
16329
- if (loadFailure) {
16330
- return { name: "Token registry", status: "fail", detail: loadFailure };
16331
- }
16332
- const locs = registry2.listLocations();
16333
- const companies = registry2.listCompanyFirebases();
16334
- const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
16335
- if (locs.length === 0) {
16336
- return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
16337
- }
16338
- return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
16339
- })();
16340
- const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
16341
- versionPromise,
16342
- apiKeyPromise,
16343
- locationPromise,
16344
- firebasePromise
16345
- ]);
16346
- let versionCheck;
16347
- if (versionStatus.latest === null) {
16348
- versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed}. Could not reach npm registry to check for updates: ${versionStatus.fetchError ?? "unknown"}.` };
16349
- } else if (versionStatus.upToDate) {
16350
- versionCheck = { name: "npm registry + version", status: "pass", detail: `Up to date at v${versionStatus.installed}.` };
16351
- } else {
16352
- versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed} but latest is v${versionStatus.latest}. Quit Claude (Cmd+Q) and reopen to upgrade.` };
16353
- }
16354
- const gatingCheck = (() => {
16355
- const r = getGatingReport?.();
16356
- if (!r) {
16357
- return { name: "Tool gating", status: "skip", detail: "Gating report unavailable in this mode." };
16358
- }
16359
- if (!r.gatingActive) {
16360
- return { name: "Tool gating", status: "pass", detail: `No gating configured \u2014 all ${r.registeredCount} tools registered. Sub-account create/delete stays OFF unless GHL_ENABLE_ACCOUNT_ADMIN=1 is set.` };
16361
- }
16362
- const parts = [`${r.registeredCount} of ${r.attemptedCount} tools registered.`];
16363
- if (r.disabledTools.length > 0) parts.push(`Disabled tools: ${r.disabledTools.join(", ")}.`);
16364
- if (r.disabledModules.length > 0) parts.push(`Disabled modules: ${r.disabledModules.join(", ")}.`);
16365
- if (r.outOfBandDenied.length > 0) parts.push(`Confirmed unregistered (setup/helper tools): ${r.outOfBandDenied.join(", ")}.`);
16366
- if (r.allowlistActive) parts.push(`Allowlist: modules=[${r.enabledModules.join(",") || "-"}], tools=[${r.enabledTools.join(",") || "-"}].`);
16367
- parts.push(`Account admin (sub-account create/delete): ${r.accountAdminEnabled ? "ENABLED via GHL_ENABLE_ACCOUNT_ADMIN=1" : "OFF (default)"}.`);
16368
- const problems = [];
16369
- if (r.ignoredHardCoreDenials.length > 0) problems.push(`recovery-core tool(s) in GHL_DISABLED_TOOLS cannot be disabled and were IGNORED: ${r.ignoredHardCoreDenials.join(", ")}`);
16370
- if (r.unknownDisabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_TOOLS (possible typo \u2014 those tools are NOT disabled): ${r.unknownDisabledTools.join(", ")}`);
16371
- if (r.unknownDisabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_MODULES (possible typo): ${r.unknownDisabledModules.join(", ")}`);
16372
- if (r.unknownEnabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_TOOLS: ${r.unknownEnabledTools.join(", ")}`);
16373
- if (r.unknownEnabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_MODULES: ${r.unknownEnabledModules.join(", ")}`);
16374
- if (problems.length > 0) {
16375
- return { name: "Tool gating", status: "warn", detail: `${parts.join(" ")} PROBLEMS: ${problems.join("; ")}. Fix the env var in your Claude config and fully restart Claude.` };
16376
- }
16377
- return { name: "Tool gating", status: "pass", detail: parts.join(" ") };
16378
- })();
16379
- const telemetryCheck = telemetryDisabled(process.env) ? {
16380
- name: "Anonymous usage stats",
16381
- status: "skip",
16382
- detail: `OFF \u2014 ${process.env.GHL_TELEMETRY?.trim() === "0" ? "GHL_TELEMETRY=0" : "DO_NOT_TRACK"} is set. Nothing is sent.`
16383
- } : {
16384
- name: "Anonymous usage stats",
16385
- status: "pass",
16386
- detail: `ON. ${TELEMETRY_DISCLOSURE} Sent: install/startup events, package version, Node major, platform, and a random device id. NEVER sent: your license key, email, GHL API key, location ID, hostname, file paths, or any GHL account data.`
16387
- };
16388
- checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck, gatingCheck, telemetryCheck);
16389
- const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
16412
+ const checks = await collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport);
16390
16413
  const lines = [];
16391
16414
  lines.push("GHL Command \u2014 Health Check");
16392
16415
  lines.push("\u2501".repeat(50));
16393
- for (const c of checks) {
16394
- lines.push(`${symbols[c.status]} ${c.name.padEnd(35)} ${c.status.toUpperCase()}`);
16395
- lines.push(` ${c.detail}`);
16396
- }
16416
+ lines.push(...renderChecks(checks));
16397
16417
  lines.push("\u2501".repeat(50));
16398
- const fails = checks.filter((c) => c.status === "fail").length;
16399
- const warns = checks.filter((c) => c.status === "warn").length;
16400
- if (fails === 0 && warns === 0) {
16401
- lines.push("All systems go.");
16402
- } else {
16403
- const parts = [];
16404
- if (fails > 0) parts.push(`${fails} failure(s)`);
16405
- if (warns > 0) parts.push(`${warns} warning(s)`);
16406
- lines.push(parts.join(", ") + " \u2014 see details above.");
16407
- }
16418
+ lines.push(summarizeChecks(checks));
16408
16419
  return { content: [{ type: "text", text: lines.join("\n") }] };
16409
16420
  }
16410
16421
  );
16411
16422
  }
16412
16423
 
16424
+ // src/tools/checkup.ts
16425
+ var import_zod54 = require("zod");
16426
+ var PROBES = [
16427
+ { key: "contacts", label: "Contacts", path: "/contacts/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["contacts"] },
16428
+ { key: "opportunities", label: "Opportunities", path: "/opportunities/search", params: { location_id: "{loc}", limit: 1 }, listKeys: ["opportunities"] },
16429
+ { key: "pipelines", label: "Pipelines", path: "/opportunities/pipelines", params: { locationId: "{loc}" }, listKeys: ["pipelines"] },
16430
+ { key: "conversations", label: "Conversations", path: "/conversations/search", params: { locationId: "{loc}", limit: 1 }, listKeys: ["conversations"] },
16431
+ { key: "calendars", label: "Calendars", path: "/calendars/", params: { locationId: "{loc}" }, listKeys: ["calendars"] },
16432
+ { key: "workflows", label: "Workflows", path: "/workflows/", params: { locationId: "{loc}" }, listKeys: ["workflows"] },
16433
+ { key: "funnels", label: "Funnels and websites", path: "/funnels/funnel/list", params: { locationId: "{loc}", limit: 1 }, listKeys: ["funnels"] },
16434
+ { key: "forms", label: "Forms", path: "/forms/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["forms"] },
16435
+ { key: "surveys", label: "Surveys", path: "/surveys/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["surveys"] },
16436
+ { key: "customFields", label: "Custom fields", path: "/locations/{loc}/customFields", listKeys: ["customFields"] },
16437
+ { key: "customValues", label: "Custom values", path: "/locations/{loc}/customValues", listKeys: ["customValues"] },
16438
+ { key: "tags", label: "Tags", path: "/locations/{loc}/tags", listKeys: ["tags"] },
16439
+ { key: "users", label: "Users", path: "/users/", params: { locationId: "{loc}" }, listKeys: ["users"] },
16440
+ { key: "customObjects", label: "Custom objects", path: "/objects/", params: { locationId: "{loc}" }, listKeys: ["objects"], optional: true },
16441
+ { key: "triggerLinks", label: "Trigger links", path: "/links/", params: { locationId: "{loc}" }, listKeys: ["links"] },
16442
+ { key: "products", label: "Products", path: "/products/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["products"] },
16443
+ // limit AND offset are required by GHL here — omitting either 422s. Same
16444
+ // params the shipped list_invoices sends (live-verified 2026-08-08).
16445
+ { key: "invoices", label: "Invoices", path: "/invoices/", params: { locationId: "{loc}", altId: "{loc}", altType: "location", limit: 1, offset: 0 }, listKeys: ["invoices"] },
16446
+ { key: "media", label: "Media library", path: "/medias/files", params: { altId: "{loc}", altType: "location", type: "file", limit: 1 }, listKeys: ["files"] },
16447
+ { key: "phoneNumbers", label: "Phone numbers", path: "/phone-system/numbers/", params: { locationId: "{loc}" }, listKeys: ["phoneNumbers"] },
16448
+ { key: "socialAccounts", label: "Social accounts", path: "/social-media-posting/{loc}/accounts", listKeys: ["accounts"], optional: true },
16449
+ // limit/offset required here too, for the same reason as invoices.
16450
+ { key: "blogAuthors", label: "Blogs", path: "/blogs/authors", params: { locationId: "{loc}", limit: 50, offset: 0 }, listKeys: ["authors"], optional: true },
16451
+ { key: "campaigns", label: "Campaigns", path: "/campaigns/", params: { locationId: "{loc}" }, listKeys: ["campaigns"], optional: true },
16452
+ { key: "courses", label: "Courses and memberships", path: "/courses/", params: { locationId: "{loc}" }, optional: true },
16453
+ { key: "businesses", label: "Businesses", path: "/businesses/", params: { locationId: "{loc}" }, listKeys: ["businesses"], optional: true }
16454
+ ];
16455
+ var BUILDER_PROBE_KEY = "workflowBuilder";
16456
+ function parseStatus(message) {
16457
+ const m = /GHL API Error (\d{3})/.exec(message);
16458
+ return m ? Number(m[1]) : null;
16459
+ }
16460
+ function classifyFailure(status, optional) {
16461
+ if (status === null) {
16462
+ return { verdict: "inconclusive", detail: "Could not reach GHL (network or timeout). Says nothing about this feature \u2014 run the checkup again." };
16463
+ }
16464
+ if (status === 401) {
16465
+ return { verdict: "no-access", detail: "401 \u2014 the API key was rejected. See the Connection section above." };
16466
+ }
16467
+ if (status === 403) {
16468
+ return { verdict: "no-access", detail: "403 \u2014 this API key's scopes don't include this feature. Fix: edit the Private Integration in GHL and tick every scope, then re-run setup." };
16469
+ }
16470
+ if (status === 429) {
16471
+ return { verdict: "inconclusive", detail: "429 \u2014 GHL rate-limited this probe. Not a fault. Re-run in a minute." };
16472
+ }
16473
+ if (status >= 500) {
16474
+ return { verdict: "inconclusive", detail: `${status} \u2014 GHL server error. Their side, not yours. Re-run later.` };
16475
+ }
16476
+ if (optional) {
16477
+ return { verdict: "unavailable", detail: `${status} \u2014 not readable on this account. Usually means the feature isn't enabled on this GHL plan.` };
16478
+ }
16479
+ return { verdict: "broken", detail: `${status} \u2014 GHL rejected a read that should work. This is what a GoHighLevel change looks like. Please send this report to support@ghlcommand.com.` };
16480
+ }
16481
+ function countRows(raw, listKeys) {
16482
+ if (typeof raw !== "object" || raw === null) return null;
16483
+ const obj = raw;
16484
+ const meta = obj.meta;
16485
+ if (typeof meta === "object" && meta !== null) {
16486
+ const total = meta.total;
16487
+ if (typeof total === "number") return total;
16488
+ }
16489
+ if (typeof obj.total === "number") return obj.total;
16490
+ for (const key of listKeys ?? []) {
16491
+ if (Array.isArray(obj[key])) return obj[key].length;
16492
+ }
16493
+ for (const value of Object.values(obj)) {
16494
+ if (Array.isArray(value)) return value.length;
16495
+ if (typeof value === "object" && value !== null) {
16496
+ for (const inner of Object.values(value)) {
16497
+ if (Array.isArray(inner)) return inner.length;
16498
+ }
16499
+ }
16500
+ }
16501
+ return null;
16502
+ }
16503
+ function substitute(value, loc) {
16504
+ return typeof value === "string" ? value.replace("{loc}", loc) : value;
16505
+ }
16506
+ var CONFIRM_ATTEMPTS = 3;
16507
+ var CONFIRM_DELAY_MS = 600;
16508
+ async function confirm(run) {
16509
+ let last = await run();
16510
+ for (let attempt = 1; attempt < CONFIRM_ATTEMPTS && last.verdict !== "working" && last.verdict !== "skipped"; attempt++) {
16511
+ await new Promise((r) => setTimeout(r, CONFIRM_DELAY_MS));
16512
+ last = await run();
16513
+ }
16514
+ if (last.verdict === "working" || last.verdict === "skipped") return last;
16515
+ return { ...last, detail: `${last.detail} (Confirmed on ${CONFIRM_ATTEMPTS} separate reads, so this is not a blip.)` };
16516
+ }
16517
+ async function runProbes(client, loc) {
16518
+ return Promise.all(
16519
+ PROBES.map((probe) => confirm(async () => {
16520
+ const path15 = probe.path.replace("{loc}", loc);
16521
+ const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
16522
+ try {
16523
+ const raw = await client.get(path15, { params, noRetry: true });
16524
+ const count = countRows(raw, probe.listKeys);
16525
+ return {
16526
+ key: probe.key,
16527
+ label: probe.label,
16528
+ verdict: "working",
16529
+ count,
16530
+ status: 200,
16531
+ detail: count === null ? "Reads fine." : count === 0 ? "Reads fine. You have none yet." : `Reads fine. ${count} in this account.`
16532
+ };
16533
+ } catch (error) {
16534
+ const message = error instanceof Error ? error.message : String(error);
16535
+ const status = parseStatus(message);
16536
+ const { verdict, detail } = classifyFailure(status, probe.optional === true);
16537
+ return { key: probe.key, label: probe.label, verdict, count: null, status, detail };
16538
+ }
16539
+ }))
16540
+ );
16541
+ }
16542
+ var BUILDER_AUTH_FAILURE = /TOKEN_EXPIRED|INVALID_ARGUMENT|UNAUTHENTICATED|token refresh|refresh token|invalid[_ ]grant|unauthorized/i;
16543
+ async function runBuilderProbe(builderClient) {
16544
+ return confirm(() => builderProbeOnce(builderClient));
16545
+ }
16546
+ async function builderProbeOnce(builderClient) {
16547
+ const base = { key: BUILDER_PROBE_KEY, label: "Workflow Builder (build + edit)", count: null, status: null };
16548
+ if (!builderClient) {
16549
+ return { ...base, verdict: "skipped", detail: "Not connected. This is step 2 of the install, not an extra. Say: Unlock the Workflow Builder." };
16550
+ }
16551
+ try {
16552
+ const raw = await builderClient.listWorkflows(1, 0);
16553
+ const count = countRows(raw, ["workflows"]);
16554
+ return { ...base, verdict: "working", status: 200, count, detail: "Connected. Claude can build and edit workflows in this account." };
16555
+ } catch (error) {
16556
+ const message = error instanceof Error ? error.message : String(error);
16557
+ const status = parseStatus(message);
16558
+ if (status === 401 || status === 403 || BUILDER_AUTH_FAILURE.test(message)) {
16559
+ return {
16560
+ ...base,
16561
+ status,
16562
+ verdict: "no-access",
16563
+ detail: "The saved GHL browser login has expired. Say: Unlock the Workflow Builder. It takes about 60 seconds and needs no pasting."
16564
+ };
16565
+ }
16566
+ if (status === null || status === 429 || status >= 500) {
16567
+ return { ...base, status, verdict: "inconclusive", detail: `Could not complete the check${status ? ` (${status})` : ""}. Says nothing either way. Run the checkup again.` };
16568
+ }
16569
+ return { ...base, status, verdict: "broken", detail: `${status} \u2014 the internal API rejected a read that should work. Please send this report to support@ghlcommand.com.` };
16570
+ }
16571
+ }
16572
+ function buildNudges(probes, builder, registeredLocations) {
16573
+ const by = new Map(probes.map((p) => [p.key, p]));
16574
+ const count = (key) => by.get(key)?.count ?? null;
16575
+ const emptyAndWorking = (key) => by.get(key)?.verdict === "working" && by.get(key)?.count === 0;
16576
+ const nudges = [];
16577
+ if (builder.verdict === "skipped" || builder.verdict === "no-access") {
16578
+ nudges.push({
16579
+ headline: "The Workflow Builder is off \u2014 that is the half that BUILDS.",
16580
+ prompt: "Unlock the Workflow Builder.",
16581
+ guide: "install-ghl-command"
16582
+ });
16583
+ }
16584
+ const workflows = count("workflows");
16585
+ if (workflows !== null && workflows > 0) {
16586
+ nudges.push({
16587
+ // Deliberately not "you have never audited these" — nothing here can
16588
+ // know that, and a checkup that overclaims loses the reader.
16589
+ headline: `${workflows} workflow(s) here. A silent-failure audit finds what GHL never warns you about.`,
16590
+ prompt: "Audit my workflows and find broken pipeline, stage, or field IDs.",
16591
+ guide: "audit-your-workflows"
16592
+ });
16593
+ } else if (emptyAndWorking("workflows")) {
16594
+ nudges.push({
16595
+ headline: "No workflows in this account \u2014 nothing follows up on its own.",
16596
+ prompt: "Build a 7-day nurture sequence with texts, emails, and waits that stops the moment they reply.",
16597
+ guide: "build-nurture-sequence"
16598
+ });
16599
+ }
16600
+ const contacts = count("contacts");
16601
+ if (emptyAndWorking("pipelines") && contacts !== null && contacts > 0) {
16602
+ nudges.push({
16603
+ headline: `${contacts} contacts and no pipeline \u2014 nothing tracks where they are.`,
16604
+ prompt: "Stand up a New Patient pipeline with stages for Lead, Booked, Showed, Treated, and Rebooked.",
16605
+ guide: "stand-up-a-pipeline"
16606
+ });
16607
+ }
16608
+ if (emptyAndWorking("calendars")) {
16609
+ nudges.push({
16610
+ headline: "No booking calendar \u2014 every appointment is being set by hand.",
16611
+ prompt: "Set up a booking calendar for me and show me the booking link.",
16612
+ guide: "set-up-a-booking-calendar"
16613
+ });
16614
+ }
16615
+ if (emptyAndWorking("forms")) {
16616
+ nudges.push({
16617
+ headline: "No forms \u2014 there is no way for a lead to raise their hand.",
16618
+ prompt: "Create an intake form for new leads and show me where the submissions land.",
16619
+ guide: "create-forms-and-read-submissions"
16620
+ });
16621
+ }
16622
+ if (emptyAndWorking("triggerLinks")) {
16623
+ nudges.push({
16624
+ headline: "No trigger links \u2014 you cannot see who clicked what.",
16625
+ prompt: "Create a trigger link and a workflow that tags anyone who clicks it.",
16626
+ guide: "trigger-links"
16627
+ });
16628
+ }
16629
+ if (emptyAndWorking("customValues")) {
16630
+ nudges.push({
16631
+ headline: "No custom values \u2014 AI bots and templates have nothing to personalize from.",
16632
+ prompt: "Set up the custom values this account needs so my templates and AI bot stop hard-coding details.",
16633
+ guide: "ai-bots-and-custom-values"
16634
+ });
16635
+ }
16636
+ if (emptyAndWorking("socialAccounts")) {
16637
+ nudges.push({
16638
+ headline: "No social accounts connected \u2014 the Social Planner is unused.",
16639
+ prompt: "Show me what I need to connect to post to social from here.",
16640
+ guide: "social-posting"
16641
+ });
16642
+ }
16643
+ if (emptyAndWorking("products") && emptyAndWorking("invoices")) {
16644
+ nudges.push({
16645
+ headline: "No products and no invoices \u2014 nothing here can take money.",
16646
+ prompt: "Set up a product and send me a test invoice so I can see the payment flow.",
16647
+ guide: "invoices-and-payments"
16648
+ });
16649
+ }
16650
+ if (registeredLocations <= 1) {
16651
+ nudges.push({
16652
+ headline: "Only one sub-account is registered \u2014 Claude can work across all of them.",
16653
+ prompt: "Register my other sub-accounts so you can switch between them by name.",
16654
+ guide: "switch-between-client-accounts"
16655
+ });
16656
+ }
16657
+ return nudges;
16658
+ }
16659
+ var VERDICT_SYMBOL = {
16660
+ working: "\u2713",
16661
+ broken: "\u2717",
16662
+ "no-access": "!",
16663
+ unavailable: "\u2014",
16664
+ inconclusive: "?",
16665
+ skipped: "\u2014"
16666
+ };
16667
+ function renderCheckup(data) {
16668
+ const lines = [];
16669
+ lines.push("GHL Command \u2014 Checkup");
16670
+ lines.push(`v${data.version} \xB7 location ${data.locationId} \xB7 ${data.generatedAt}`);
16671
+ lines.push("\u2501".repeat(58));
16672
+ lines.push("");
16673
+ lines.push("1. CONNECTION \u2014 is the install wired up?");
16674
+ lines.push("\u2500".repeat(58));
16675
+ lines.push(...renderChecks(data.install));
16676
+ lines.push(summarizeChecks(data.install));
16677
+ lines.push("");
16678
+ lines.push("2. WHAT STILL WORKS \u2014 every capability read live, just now");
16679
+ lines.push("\u2500".repeat(58));
16680
+ for (const p of data.probes) {
16681
+ lines.push(`${VERDICT_SYMBOL[p.verdict]} ${p.label.padEnd(30)} ${p.verdict.toUpperCase()}`);
16682
+ lines.push(` ${p.detail}`);
16683
+ }
16684
+ const broken = data.probes.filter((p) => p.verdict === "broken");
16685
+ lines.push("");
16686
+ if (broken.length === 0) {
16687
+ lines.push("No breakage found. Every capability GHL Command claims still answers.");
16688
+ } else {
16689
+ lines.push(`\u26A0 ${broken.length} capability area(s) are BROKEN: ${broken.map((p) => p.label).join(", ")}.`);
16690
+ lines.push(" This is almost always a GoHighLevel change, not something you did.");
16691
+ lines.push(" Send this whole report to support@ghlcommand.com \u2014 it is exactly what we need to fix it.");
16692
+ }
16693
+ lines.push("");
16694
+ lines.push("3. WHAT YOU ARE NOT USING YET");
16695
+ lines.push("\u2500".repeat(58));
16696
+ lines.push(`Actively using ${data.inUse} of ${data.usable} capability areas in this account.`);
16697
+ lines.push("(Measured from what this GHL account contains \u2014 nothing about your usage is collected or sent.)");
16698
+ lines.push("");
16699
+ if (data.nudges.length === 0) {
16700
+ lines.push("Nothing obvious left on the table. This account is being worked hard.");
16701
+ } else {
16702
+ data.nudges.forEach((n, i) => {
16703
+ lines.push(`${i + 1}. ${n.headline}`);
16704
+ lines.push(` Say: "${n.prompt}"`);
16705
+ if (n.guide) lines.push(` Guide: ${n.guide} \u2014 open the full library with "open the user guide".`);
16706
+ });
16707
+ }
16708
+ lines.push("");
16709
+ lines.push("\u2501".repeat(58));
16710
+ lines.push("Run this monthly, and any time GHL feels off. Questions: support@ghlcommand.com");
16711
+ return lines.join("\n");
16712
+ }
16713
+ function registerCheckupTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
16714
+ server2.tool(
16715
+ "run_checkup",
16716
+ `The full GHL Command checkup \u2014 the answer to "am I using it all, and is it all still working?". Three sections: (1) CONNECTION, the same install checks as health_check; (2) WHAT STILL WORKS, one live read-only probe against every GHL capability family, which distinguishes "working but you own none" from "GoHighLevel changed something and this is broken"; (3) WHAT YOU ARE NOT USING YET, derived from what the account actually contains, each line carrying the exact sentence to paste back. Read-only and safe on live client accounts. Use format:'json' for a machine-readable report. Run it monthly, after a GHL update, or any time something feels off.`,
16717
+ {
16718
+ locationId: import_zod54.z.string().optional().describe("Sub-account to check. Defaults to the active location."),
16719
+ format: import_zod54.z.enum(["report", "json"]).optional().describe("'report' (default) is the plain-English checkup. 'json' returns the same data for scripts and support tickets.")
16720
+ },
16721
+ async ({ locationId: locationId2, format }) => {
16722
+ let loc;
16723
+ try {
16724
+ loc = client.resolveLocationId(locationId2);
16725
+ } catch (error) {
16726
+ return {
16727
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
16728
+ isError: true
16729
+ };
16730
+ }
16731
+ const [install, probes, builder] = await Promise.all([
16732
+ collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport),
16733
+ runProbes(client, loc),
16734
+ runBuilderProbe(builderClient)
16735
+ ]);
16736
+ const allProbes = [...probes, builder];
16737
+ const usable = allProbes.filter((p) => p.verdict === "working").length;
16738
+ const inUse = allProbes.filter((p) => p.verdict === "working" && (p.count === null || p.count > 0)).length;
16739
+ const data = {
16740
+ version: installedVersion,
16741
+ locationId: loc,
16742
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16743
+ install,
16744
+ probes: allProbes,
16745
+ nudges: buildNudges(probes, builder, registry2?.listLocations().length ?? 0),
16746
+ inUse,
16747
+ usable
16748
+ };
16749
+ const text = format === "json" ? JSON.stringify(data, null, 2) : renderCheckup(data);
16750
+ return { content: [{ type: "text", text }] };
16751
+ }
16752
+ );
16753
+ }
16754
+
16413
16755
  // src/tools/user-guide.ts
16414
16756
  var import_child_process2 = require("child_process");
16415
16757
  var fs8 = __toESM(require("fs"));
@@ -16462,11 +16804,11 @@ ${guidePath}`;
16462
16804
  }
16463
16805
 
16464
16806
  // src/tools/snapshots.ts
16465
- var import_zod54 = require("zod");
16807
+ var import_zod55 = require("zod");
16466
16808
  init_ghl_client();
16467
- var SnapshotSchema = import_zod54.z.object({ id: import_zod54.z.string(), name: import_zod54.z.string(), type: import_zod54.z.string() }).passthrough();
16468
- var SnapshotsResponseSchema = import_zod54.z.object({ snapshots: import_zod54.z.array(SnapshotSchema) }).passthrough();
16469
- var ShareLinkResponseSchema = import_zod54.z.object({ id: import_zod54.z.string(), shareLink: import_zod54.z.string() }).passthrough();
16809
+ var SnapshotSchema = import_zod55.z.object({ id: import_zod55.z.string(), name: import_zod55.z.string(), type: import_zod55.z.string() }).passthrough();
16810
+ var SnapshotsResponseSchema = import_zod55.z.object({ snapshots: import_zod55.z.array(SnapshotSchema) }).passthrough();
16811
+ var ShareLinkResponseSchema = import_zod55.z.object({ id: import_zod55.z.string(), shareLink: import_zod55.z.string() }).passthrough();
16470
16812
  var SHARE_TYPES = [
16471
16813
  "link",
16472
16814
  "permanent_link",
@@ -16513,7 +16855,7 @@ function registerSnapshotTools(server2, client, registry2) {
16513
16855
  "list_snapshots",
16514
16856
  "List the agency's GHL snapshots (id, name, type) so you can pick the right one by name before applying it to a sub-account. Reads the agency/company-scoped key (not a sub-account PIT). companyId defaults to the active location's company; pass it explicitly to target a different company you have the agency key for. Read-only.",
16515
16857
  {
16516
- companyId: import_zod54.z.string().optional().describe(
16858
+ companyId: import_zod55.z.string().optional().describe(
16517
16859
  "Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
16518
16860
  )
16519
16861
  },
@@ -16538,11 +16880,11 @@ function registerSnapshotTools(server2, client, registry2) {
16538
16880
  "create_snapshot_share_link",
16539
16881
  "Create a shareable load link for one of the agency's snapshots (returns a gohighlevel.com/?share=... URL to import the snapshot into a sub-account). Uses the agency/company-scoped key. WARNING: this is NOT idempotent \u2014 each call mints a NEW link, and there is no API to list or revoke links (revoke in the GHL UI under the snapshot's share settings). Pick share_type deliberately. Use list_snapshots first to get the snapshot id.",
16540
16882
  {
16541
- snapshot_id: import_zod54.z.string().describe("The snapshot id to share (from list_snapshots)."),
16542
- share_type: import_zod54.z.enum(SHARE_TYPES).describe(
16883
+ snapshot_id: import_zod55.z.string().describe("The snapshot id to share (from list_snapshots)."),
16884
+ share_type: import_zod55.z.enum(SHARE_TYPES).describe(
16543
16885
  "Share link type. 'link' = standard share link; 'permanent_link' = non-expiring; 'agency_link' = share to agencies; 'location_link' = load into a sub-account/location; 'marketplace_link' = marketplace listing. No default \u2014 choose intentionally."
16544
16886
  ),
16545
- companyId: import_zod54.z.string().optional().describe(
16887
+ companyId: import_zod55.z.string().optional().describe(
16546
16888
  "Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
16547
16889
  )
16548
16890
  },
@@ -16572,17 +16914,17 @@ function registerSnapshotTools(server2, client, registry2) {
16572
16914
  }
16573
16915
 
16574
16916
  // src/tools/phone.ts
16575
- var import_zod55 = require("zod");
16576
- var PhoneNumberSchema = import_zod55.z.object({ sid: import_zod55.z.string(), value: import_zod55.z.string(), title: import_zod55.z.string().optional() }).passthrough();
16577
- var NumbersResponseSchema = import_zod55.z.object({ phoneNumbers: import_zod55.z.array(PhoneNumberSchema) }).passthrough();
16578
- var PoolsResponseSchema = import_zod55.z.object({ pools: import_zod55.z.array(import_zod55.z.object({}).passthrough()) }).passthrough();
16917
+ var import_zod56 = require("zod");
16918
+ var PhoneNumberSchema = import_zod56.z.object({ sid: import_zod56.z.string(), value: import_zod56.z.string(), title: import_zod56.z.string().optional() }).passthrough();
16919
+ var NumbersResponseSchema = import_zod56.z.object({ phoneNumbers: import_zod56.z.array(PhoneNumberSchema) }).passthrough();
16920
+ var PoolsResponseSchema = import_zod56.z.object({ pools: import_zod56.z.array(import_zod56.z.object({}).passthrough()) }).passthrough();
16579
16921
  function registerPhoneTools(server2, client) {
16580
16922
  safeTool(
16581
16923
  server2,
16582
16924
  "list_phone_numbers",
16583
16925
  "List the LC Phone numbers provisioned for a location (sid, number, label). Read-only. Use to verify or count purchased numbers (e.g. provisioning step 11). Number purchase is not exposed (billable write).",
16584
16926
  {
16585
- locationId: import_zod55.z.string().optional().describe("Defaults to the active location.")
16927
+ locationId: import_zod56.z.string().optional().describe("Defaults to the active location.")
16586
16928
  },
16587
16929
  async ({ locationId: locationId2 }) => {
16588
16930
  const loc = client.resolveLocationId(locationId2);
@@ -16600,7 +16942,7 @@ function registerPhoneTools(server2, client) {
16600
16942
  "list_number_pools",
16601
16943
  "List LC Phone number pools configured for a location. Read-only.",
16602
16944
  {
16603
- locationId: import_zod55.z.string().optional().describe("Defaults to the active location.")
16945
+ locationId: import_zod56.z.string().optional().describe("Defaults to the active location.")
16604
16946
  },
16605
16947
  async ({ locationId: locationId2 }) => {
16606
16948
  const loc = client.resolveLocationId(locationId2);
@@ -16612,10 +16954,10 @@ function registerPhoneTools(server2, client) {
16612
16954
  }
16613
16955
 
16614
16956
  // src/tools/account-health.ts
16615
- var import_zod56 = require("zod");
16616
- var MetaTotalSchema = import_zod56.z.object({ meta: import_zod56.z.object({ total: import_zod56.z.number() }).passthrough() }).passthrough();
16617
- var TotalSchema2 = import_zod56.z.object({ total: import_zod56.z.number() }).passthrough();
16618
- var NumbersSchema = import_zod56.z.object({ phoneNumbers: import_zod56.z.array(import_zod56.z.unknown()) }).passthrough();
16957
+ var import_zod57 = require("zod");
16958
+ var MetaTotalSchema = import_zod57.z.object({ meta: import_zod57.z.object({ total: import_zod57.z.number() }).passthrough() }).passthrough();
16959
+ var TotalSchema2 = import_zod57.z.object({ total: import_zod57.z.number() }).passthrough();
16960
+ var NumbersSchema = import_zod57.z.object({ phoneNumbers: import_zod57.z.array(import_zod57.z.unknown()) }).passthrough();
16619
16961
  var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
16620
16962
  async function section(scope, fn) {
16621
16963
  try {
@@ -16630,8 +16972,8 @@ function registerAccountHealthTools(server2, client) {
16630
16972
  "get_account_health_summary",
16631
16973
  "Account-health summary for a location, composed from existing reads (GHL has no reporting API). Returns: total contacts + NEW contacts in the window; total opportunities + counts by status (open/won/lost/abandoned); total conversations; phone-number count. Every metric is explicitly labeled all_time vs window (with start/end) \u2014 windowed and all-time numbers are never conflated. Sections that can't be read return status:'unavailable' (never a misleading 0). Revenue and appointments are intentionally excluded (not reachable / too costly via the public API).",
16632
16974
  {
16633
- locationId: import_zod56.z.string().optional().describe("Defaults to the active location."),
16634
- windowDays: import_zod56.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
16975
+ locationId: import_zod57.z.string().optional().describe("Defaults to the active location."),
16976
+ windowDays: import_zod57.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
16635
16977
  },
16636
16978
  async ({ locationId: locationId2, windowDays }) => {
16637
16979
  const loc = client.resolveLocationId(locationId2);
@@ -16697,7 +17039,7 @@ function registerAccountHealthTools(server2, client) {
16697
17039
  }
16698
17040
 
16699
17041
  // src/tools/intake-to-build.ts
16700
- var import_zod59 = require("zod");
17042
+ var import_zod60 = require("zod");
16701
17043
 
16702
17044
  // src/intake-to-build/form-template.ts
16703
17045
  init_question_set();
@@ -16981,7 +17323,7 @@ init_question_set();
16981
17323
  init_question_set();
16982
17324
 
16983
17325
  // src/intake-to-build/brief.ts
16984
- var import_zod57 = require("zod");
17326
+ var import_zod58 = require("zod");
16985
17327
  var BRIEF_SCHEMA_VERSION = "0.1";
16986
17328
  var PRESETS = [
16987
17329
  "generic",
@@ -16991,67 +17333,67 @@ var PRESETS = [
16991
17333
  "ecom",
16992
17334
  "agency"
16993
17335
  ];
16994
- var presetSchema = import_zod57.z.enum(PRESETS);
17336
+ var presetSchema = import_zod58.z.enum(PRESETS);
16995
17337
  var BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
16996
- var briefSourceSchema = import_zod57.z.enum(BRIEF_SOURCES);
16997
- var pricePointSchema = import_zod57.z.object({
16998
- name: import_zod57.z.string(),
16999
- price: import_zod57.z.string()
17338
+ var briefSourceSchema = import_zod58.z.enum(BRIEF_SOURCES);
17339
+ var pricePointSchema = import_zod58.z.object({
17340
+ name: import_zod58.z.string(),
17341
+ price: import_zod58.z.string()
17000
17342
  });
17001
- var briefSchema = import_zod57.z.object({
17002
- schemaVersion: import_zod57.z.string(),
17003
- briefId: import_zod57.z.string(),
17343
+ var briefSchema = import_zod58.z.object({
17344
+ schemaVersion: import_zod58.z.string(),
17345
+ briefId: import_zod58.z.string(),
17004
17346
  preset: presetSchema,
17005
17347
  briefSource: briefSourceSchema,
17006
17348
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
17007
- extended: import_zod57.z.record(import_zod57.z.unknown()).optional(),
17008
- business: import_zod57.z.object({
17009
- name: import_zod57.z.string(),
17010
- type: import_zod57.z.string().optional(),
17011
- website: import_zod57.z.string().optional(),
17012
- location: import_zod57.z.string().optional(),
17013
- timezone: import_zod57.z.string().optional(),
17349
+ extended: import_zod58.z.record(import_zod58.z.unknown()).optional(),
17350
+ business: import_zod58.z.object({
17351
+ name: import_zod58.z.string(),
17352
+ type: import_zod58.z.string().optional(),
17353
+ website: import_zod58.z.string().optional(),
17354
+ location: import_zod58.z.string().optional(),
17355
+ timezone: import_zod58.z.string().optional(),
17014
17356
  // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
17015
17357
  // the same tolerance reason as business.type (don't reject valid briefs).
17016
- teamSize: import_zod57.z.string().optional(),
17017
- monthlyLeadVolume: import_zod57.z.string().optional(),
17018
- hours: import_zod57.z.string().optional()
17358
+ teamSize: import_zod58.z.string().optional(),
17359
+ monthlyLeadVolume: import_zod58.z.string().optional(),
17360
+ hours: import_zod58.z.string().optional()
17019
17361
  }).passthrough(),
17020
- offer: import_zod57.z.object({
17021
- summary: import_zod57.z.string().optional(),
17362
+ offer: import_zod58.z.object({
17363
+ summary: import_zod58.z.string().optional(),
17022
17364
  // Parsed best-effort; tolerate a raw string when parsing was not possible.
17023
- pricePoints: import_zod57.z.union([import_zod57.z.array(pricePointSchema), import_zod57.z.string()]).optional(),
17024
- leadMagnet: import_zod57.z.string().optional(),
17025
- avgDealValue: import_zod57.z.string().optional()
17365
+ pricePoints: import_zod58.z.union([import_zod58.z.array(pricePointSchema), import_zod58.z.string()]).optional(),
17366
+ leadMagnet: import_zod58.z.string().optional(),
17367
+ avgDealValue: import_zod58.z.string().optional()
17026
17368
  }).passthrough().optional(),
17027
- audience: import_zod57.z.object({
17028
- ideal: import_zod57.z.string().optional(),
17029
- painPoints: import_zod57.z.array(import_zod57.z.string()).optional(),
17030
- objections: import_zod57.z.array(import_zod57.z.string()).optional()
17369
+ audience: import_zod58.z.object({
17370
+ ideal: import_zod58.z.string().optional(),
17371
+ painPoints: import_zod58.z.array(import_zod58.z.string()).optional(),
17372
+ objections: import_zod58.z.array(import_zod58.z.string()).optional()
17031
17373
  }).passthrough().optional(),
17032
- goal: import_zod57.z.object({
17374
+ goal: import_zod58.z.object({
17033
17375
  // Kept as string: the form option labels are the canonical values, but
17034
17376
  // the contract sample shortens some (e.g. "high-touch"). See §7 note.
17035
- primary: import_zod57.z.string(),
17036
- salesStages: import_zod57.z.array(import_zod57.z.string()).optional(),
17037
- bookingNeeded: import_zod57.z.boolean().optional(),
17038
- followUpStyle: import_zod57.z.string().optional()
17377
+ primary: import_zod58.z.string(),
17378
+ salesStages: import_zod58.z.array(import_zod58.z.string()).optional(),
17379
+ bookingNeeded: import_zod58.z.boolean().optional(),
17380
+ followUpStyle: import_zod58.z.string().optional()
17039
17381
  }).passthrough(),
17040
- channels: import_zod57.z.object({
17041
- email: import_zod57.z.boolean().optional(),
17042
- sms: import_zod57.z.boolean().optional(),
17043
- a2pStatus: import_zod57.z.string().optional(),
17044
- payment: import_zod57.z.string().optional(),
17045
- calendarConnected: import_zod57.z.boolean().optional(),
17046
- social: import_zod57.z.array(import_zod57.z.string()).optional()
17382
+ channels: import_zod58.z.object({
17383
+ email: import_zod58.z.boolean().optional(),
17384
+ sms: import_zod58.z.boolean().optional(),
17385
+ a2pStatus: import_zod58.z.string().optional(),
17386
+ payment: import_zod58.z.string().optional(),
17387
+ calendarConnected: import_zod58.z.boolean().optional(),
17388
+ social: import_zod58.z.array(import_zod58.z.string()).optional()
17047
17389
  }).passthrough().optional(),
17048
- assets: import_zod57.z.object({
17049
- existingPipeline: import_zod57.z.string().optional(),
17050
- existingWorkflows: import_zod57.z.string().optional(),
17051
- brand: import_zod57.z.string().optional(),
17052
- notes: import_zod57.z.string().optional()
17390
+ assets: import_zod58.z.object({
17391
+ existingPipeline: import_zod58.z.string().optional(),
17392
+ existingWorkflows: import_zod58.z.string().optional(),
17393
+ brand: import_zod58.z.string().optional(),
17394
+ notes: import_zod58.z.string().optional()
17053
17395
  }).passthrough().optional(),
17054
- flags: import_zod57.z.array(import_zod57.z.string()).optional()
17396
+ flags: import_zod58.z.array(import_zod58.z.string()).optional()
17055
17397
  }).strict();
17056
17398
  function validateBrief(input) {
17057
17399
  const parsed = briefSchema.safeParse(input);
@@ -17209,7 +17551,7 @@ function normalizeSubmissionToBrief(opts) {
17209
17551
  }
17210
17552
 
17211
17553
  // src/intake-to-build/plan.ts
17212
- var import_zod58 = require("zod");
17554
+ var import_zod59 = require("zod");
17213
17555
  var REF_NAMESPACES = [
17214
17556
  "pipeline",
17215
17557
  "stage",
@@ -17226,22 +17568,22 @@ var REF_NAMESPACES = [
17226
17568
  "handoff"
17227
17569
  ];
17228
17570
  var REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
17229
- var refSchema = import_zod58.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
17571
+ var refSchema = import_zod59.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
17230
17572
  function nsRef(ns) {
17231
- return import_zod58.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
17573
+ return import_zod59.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
17232
17574
  }
17233
17575
  function refNamespace(ref) {
17234
17576
  return ref.split(".")[0];
17235
17577
  }
17236
- var stageSchema = import_zod58.z.object({
17578
+ var stageSchema = import_zod59.z.object({
17237
17579
  ref: nsRef("stage"),
17238
- name: import_zod58.z.string(),
17239
- position: import_zod58.z.number().int().nonnegative()
17580
+ name: import_zod59.z.string(),
17581
+ position: import_zod59.z.number().int().nonnegative()
17240
17582
  });
17241
- var pipelineSchema = import_zod58.z.object({
17583
+ var pipelineSchema = import_zod59.z.object({
17242
17584
  ref: nsRef("pipeline"),
17243
- name: import_zod58.z.string(),
17244
- stages: import_zod58.z.array(stageSchema).min(1)
17585
+ name: import_zod59.z.string(),
17586
+ stages: import_zod59.z.array(stageSchema).min(1)
17245
17587
  });
17246
17588
  var GHL_FIELD_DATATYPES = [
17247
17589
  "TEXT",
@@ -17258,21 +17600,21 @@ var GHL_FIELD_DATATYPES = [
17258
17600
  "FILE_UPLOAD",
17259
17601
  "SIGNATURE"
17260
17602
  ];
17261
- var customFieldSchema = import_zod58.z.object({
17603
+ var customFieldSchema = import_zod59.z.object({
17262
17604
  ref: nsRef("field"),
17263
- name: import_zod58.z.string(),
17264
- dataType: import_zod58.z.enum(GHL_FIELD_DATATYPES),
17265
- model: import_zod58.z.enum(["contact", "opportunity"]).optional(),
17266
- options: import_zod58.z.array(import_zod58.z.string()).optional()
17605
+ name: import_zod59.z.string(),
17606
+ dataType: import_zod59.z.enum(GHL_FIELD_DATATYPES),
17607
+ model: import_zod59.z.enum(["contact", "opportunity"]).optional(),
17608
+ options: import_zod59.z.array(import_zod59.z.string()).optional()
17267
17609
  });
17268
- var tagSchema = import_zod58.z.object({
17610
+ var tagSchema = import_zod59.z.object({
17269
17611
  ref: nsRef("tag"),
17270
- name: import_zod58.z.string()
17612
+ name: import_zod59.z.string()
17271
17613
  });
17272
- var customValueSchema = import_zod58.z.object({
17614
+ var customValueSchema = import_zod59.z.object({
17273
17615
  ref: nsRef("cv"),
17274
- name: import_zod58.z.string(),
17275
- value: import_zod58.z.string().optional(),
17616
+ name: import_zod59.z.string(),
17617
+ value: import_zod59.z.string().optional(),
17276
17618
  filledBy: refSchema.optional()
17277
17619
  });
17278
17620
  var CALENDAR_TYPES = [
@@ -17282,89 +17624,89 @@ var CALENDAR_TYPES = [
17282
17624
  "collective",
17283
17625
  "service_booking"
17284
17626
  ];
17285
- var openHoursBlockSchema = import_zod58.z.object({
17286
- daysOfTheWeek: import_zod58.z.array(import_zod58.z.number().int().min(0).max(6)),
17287
- hours: import_zod58.z.array(
17288
- import_zod58.z.object({
17289
- openHour: import_zod58.z.number().int().min(0).max(23),
17290
- openMinute: import_zod58.z.number().int().min(0).max(59),
17291
- closeHour: import_zod58.z.number().int().min(0).max(23),
17292
- closeMinute: import_zod58.z.number().int().min(0).max(59)
17627
+ var openHoursBlockSchema = import_zod59.z.object({
17628
+ daysOfTheWeek: import_zod59.z.array(import_zod59.z.number().int().min(0).max(6)),
17629
+ hours: import_zod59.z.array(
17630
+ import_zod59.z.object({
17631
+ openHour: import_zod59.z.number().int().min(0).max(23),
17632
+ openMinute: import_zod59.z.number().int().min(0).max(59),
17633
+ closeHour: import_zod59.z.number().int().min(0).max(23),
17634
+ closeMinute: import_zod59.z.number().int().min(0).max(59)
17293
17635
  })
17294
17636
  )
17295
17637
  });
17296
- var calendarSchema = import_zod58.z.object({
17638
+ var calendarSchema = import_zod59.z.object({
17297
17639
  ref: nsRef("calendar"),
17298
- name: import_zod58.z.string(),
17299
- calendarType: import_zod58.z.enum(CALENDAR_TYPES),
17300
- openHours: import_zod58.z.array(openHoursBlockSchema).optional(),
17301
- availabilityType: import_zod58.z.number().int().optional(),
17302
- requiresStaff: import_zod58.z.boolean().optional()
17640
+ name: import_zod59.z.string(),
17641
+ calendarType: import_zod59.z.enum(CALENDAR_TYPES),
17642
+ openHours: import_zod59.z.array(openHoursBlockSchema).optional(),
17643
+ availabilityType: import_zod59.z.number().int().optional(),
17644
+ requiresStaff: import_zod59.z.boolean().optional()
17303
17645
  });
17304
- var formFieldSchema = import_zod58.z.discriminatedUnion("type", [
17305
- import_zod58.z.object({
17306
- type: import_zod58.z.literal("standard"),
17307
- key: import_zod58.z.string(),
17308
- required: import_zod58.z.boolean().optional()
17646
+ var formFieldSchema = import_zod59.z.discriminatedUnion("type", [
17647
+ import_zod59.z.object({
17648
+ type: import_zod59.z.literal("standard"),
17649
+ key: import_zod59.z.string(),
17650
+ required: import_zod59.z.boolean().optional()
17309
17651
  }),
17310
- import_zod58.z.object({
17311
- type: import_zod58.z.literal("custom"),
17652
+ import_zod59.z.object({
17653
+ type: import_zod59.z.literal("custom"),
17312
17654
  fieldRef: nsRef("field"),
17313
- required: import_zod58.z.boolean().optional()
17655
+ required: import_zod59.z.boolean().optional()
17314
17656
  })
17315
17657
  ]);
17316
- var formSchema = import_zod58.z.object({
17658
+ var formSchema = import_zod59.z.object({
17317
17659
  ref: nsRef("form"),
17318
- name: import_zod58.z.string(),
17319
- fields: import_zod58.z.array(formFieldSchema)
17660
+ name: import_zod59.z.string(),
17661
+ fields: import_zod59.z.array(formFieldSchema)
17320
17662
  });
17321
- var pageSchema = import_zod58.z.object({
17663
+ var pageSchema = import_zod59.z.object({
17322
17664
  ref: nsRef("page"),
17323
- name: import_zod58.z.string(),
17324
- role: import_zod58.z.string().optional(),
17325
- outline: import_zod58.z.string().optional(),
17665
+ name: import_zod59.z.string(),
17666
+ role: import_zod59.z.string().optional(),
17667
+ outline: import_zod59.z.string().optional(),
17326
17668
  formRef: nsRef("form").optional(),
17327
17669
  calendarRef: nsRef("calendar").optional()
17328
17670
  });
17329
17671
  var FUNNEL_TARGETS = ["ghl", "external"];
17330
17672
  var FUNNEL_HOSTS = ["cloudflare", "vercel"];
17331
- var funnelSchema = import_zod58.z.object({
17673
+ var funnelSchema = import_zod59.z.object({
17332
17674
  ref: nsRef("funnel"),
17333
- name: import_zod58.z.string(),
17675
+ name: import_zod59.z.string(),
17334
17676
  // Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
17335
17677
  // "external" = the subscriber builds + hosts the site themselves (Cloudflare/
17336
17678
  // Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
17337
17679
  // see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
17338
17680
  // deploy an external funnel; it surfaces the GHL-side wiring info.
17339
- target: import_zod58.z.enum(FUNNEL_TARGETS).optional(),
17340
- host: import_zod58.z.enum(FUNNEL_HOSTS).optional(),
17681
+ target: import_zod59.z.enum(FUNNEL_TARGETS).optional(),
17682
+ host: import_zod59.z.enum(FUNNEL_HOSTS).optional(),
17341
17683
  // external only
17342
- domain: import_zod58.z.string().optional(),
17684
+ domain: import_zod59.z.string().optional(),
17343
17685
  // external only
17344
- pages: import_zod58.z.array(pageSchema)
17686
+ pages: import_zod59.z.array(pageSchema)
17345
17687
  });
17346
- var emailAssetSchema = import_zod58.z.object({
17688
+ var emailAssetSchema = import_zod59.z.object({
17347
17689
  ref: nsRef("email"),
17348
- name: import_zod58.z.string(),
17349
- subject: import_zod58.z.string().optional(),
17350
- bodyOutline: import_zod58.z.string().optional(),
17351
- body: import_zod58.z.string().optional(),
17352
- mergeTags: import_zod58.z.array(import_zod58.z.string()).optional()
17690
+ name: import_zod59.z.string(),
17691
+ subject: import_zod59.z.string().optional(),
17692
+ bodyOutline: import_zod59.z.string().optional(),
17693
+ body: import_zod59.z.string().optional(),
17694
+ mergeTags: import_zod59.z.array(import_zod59.z.string()).optional()
17353
17695
  });
17354
- var smsAssetSchema = import_zod58.z.object({
17696
+ var smsAssetSchema = import_zod59.z.object({
17355
17697
  ref: nsRef("sms"),
17356
- name: import_zod58.z.string(),
17357
- bodyOutline: import_zod58.z.string().optional(),
17358
- body: import_zod58.z.string().optional(),
17359
- mergeTags: import_zod58.z.array(import_zod58.z.string()).optional()
17698
+ name: import_zod59.z.string(),
17699
+ bodyOutline: import_zod59.z.string().optional(),
17700
+ body: import_zod59.z.string().optional(),
17701
+ mergeTags: import_zod59.z.array(import_zod59.z.string()).optional()
17360
17702
  });
17361
- var waitUnit = import_zod58.z.enum(["minutes", "hours", "days"]);
17703
+ var waitUnit = import_zod59.z.enum(["minutes", "hours", "days"]);
17362
17704
  var branchActionOptions = [
17363
- import_zod58.z.object({ type: import_zod58.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
17364
- import_zod58.z.object({ type: import_zod58.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
17365
- import_zod58.z.object({ type: import_zod58.z.literal("send_email"), emailRef: nsRef("email") }),
17366
- import_zod58.z.object({ type: import_zod58.z.literal("send_sms"), smsRef: nsRef("sms") }),
17367
- import_zod58.z.object({ type: import_zod58.z.literal("wait"), value: import_zod58.z.number().positive(), unit: waitUnit }),
17705
+ import_zod59.z.object({ type: import_zod59.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
17706
+ import_zod59.z.object({ type: import_zod59.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
17707
+ import_zod59.z.object({ type: import_zod59.z.literal("send_email"), emailRef: nsRef("email") }),
17708
+ import_zod59.z.object({ type: import_zod59.z.literal("send_sms"), smsRef: nsRef("sms") }),
17709
+ import_zod59.z.object({ type: import_zod59.z.literal("wait"), value: import_zod59.z.number().positive(), unit: waitUnit }),
17368
17710
  // Appointment-relative wait ("wait until N BEFORE the appointment").
17369
17711
  // Only works when the workflow has an appointment in context (i.e. an
17370
17712
  // `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
@@ -17373,80 +17715,80 @@ var branchActionOptions = [
17373
17715
  // (GHL stores whole minutes). Only "before" is emitted today — that's the
17374
17716
  // shape we captured + proved; "after" (post-appointment follow-up) is
17375
17717
  // deferred until its shape is captured from a real workflow.
17376
- import_zod58.z.object({
17377
- type: import_zod58.z.literal("wait_appointment"),
17378
- value: import_zod58.z.number().int().positive(),
17718
+ import_zod59.z.object({
17719
+ type: import_zod59.z.literal("wait_appointment"),
17720
+ value: import_zod59.z.number().int().positive(),
17379
17721
  unit: waitUnit
17380
17722
  }),
17381
- import_zod58.z.object({
17382
- type: import_zod58.z.literal("internal_notification"),
17383
- to: import_zod58.z.string(),
17384
- title: import_zod58.z.string(),
17385
- body: import_zod58.z.string()
17723
+ import_zod59.z.object({
17724
+ type: import_zod59.z.literal("internal_notification"),
17725
+ to: import_zod59.z.string(),
17726
+ title: import_zod59.z.string(),
17727
+ body: import_zod59.z.string()
17386
17728
  }),
17387
- import_zod58.z.object({
17388
- type: import_zod58.z.literal("update_contact_field"),
17729
+ import_zod59.z.object({
17730
+ type: import_zod59.z.literal("update_contact_field"),
17389
17731
  fieldRef: nsRef("field"),
17390
- value: import_zod58.z.string()
17732
+ value: import_zod59.z.string()
17391
17733
  }),
17392
- import_zod58.z.object({ type: import_zod58.z.literal("add_notes"), body: import_zod58.z.string() }),
17393
- import_zod58.z.object({
17394
- type: import_zod58.z.literal("task_notification"),
17395
- title: import_zod58.z.string(),
17396
- body: import_zod58.z.string().optional(),
17397
- dueDate: import_zod58.z.string().optional(),
17398
- assignedTo: import_zod58.z.string().optional()
17734
+ import_zod59.z.object({ type: import_zod59.z.literal("add_notes"), body: import_zod59.z.string() }),
17735
+ import_zod59.z.object({
17736
+ type: import_zod59.z.literal("task_notification"),
17737
+ title: import_zod59.z.string(),
17738
+ body: import_zod59.z.string().optional(),
17739
+ dueDate: import_zod59.z.string().optional(),
17740
+ assignedTo: import_zod59.z.string().optional()
17399
17741
  }),
17400
- import_zod58.z.object({ type: import_zod58.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
17401
- import_zod58.z.object({ type: import_zod58.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
17402
- import_zod58.z.object({
17403
- type: import_zod58.z.literal("create_opportunity"),
17742
+ import_zod59.z.object({ type: import_zod59.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
17743
+ import_zod59.z.object({ type: import_zod59.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
17744
+ import_zod59.z.object({
17745
+ type: import_zod59.z.literal("create_opportunity"),
17404
17746
  pipelineRef: nsRef("pipeline"),
17405
17747
  stageRef: nsRef("stage"),
17406
17748
  // Opportunity name (merge fields allowed). Defaults to the contact's name.
17407
17749
  // Required by GHL's create node; without it the create silently no-ops.
17408
- name: import_zod58.z.string().optional(),
17750
+ name: import_zod59.z.string().optional(),
17409
17751
  // Opportunity monetary value (the deal/sale dollar amount). A string so it
17410
17752
  // can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
17411
17753
  // Optional — omitted → GHL leaves the value unset. Shape captured from Lux
17412
17754
  // Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
17413
17755
  // won/lost status, so a "won" opp = move to the closing stage WITH this value.
17414
- value: import_zod58.z.string().optional()
17756
+ value: import_zod59.z.string().optional()
17415
17757
  }),
17416
- import_zod58.z.object({
17417
- type: import_zod58.z.literal("update_opportunity"),
17758
+ import_zod59.z.object({
17759
+ type: import_zod59.z.literal("update_opportunity"),
17418
17760
  pipelineRef: nsRef("pipeline"),
17419
17761
  stageRef: nsRef("stage"),
17420
17762
  // Opportunity monetary value (see create_opportunity.value). Optional.
17421
- value: import_zod58.z.string().optional()
17763
+ value: import_zod59.z.string().optional()
17422
17764
  }),
17423
- import_zod58.z.object({
17424
- type: import_zod58.z.literal("goal_event"),
17425
- goalCondition: import_zod58.z.string(),
17765
+ import_zod59.z.object({
17766
+ type: import_zod59.z.literal("goal_event"),
17767
+ goalCondition: import_zod59.z.string(),
17426
17768
  // GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
17427
- action: import_zod58.z.enum(["exit", "continue", "wait"]).optional()
17769
+ action: import_zod59.z.enum(["exit", "continue", "wait"]).optional()
17428
17770
  })
17429
17771
  ];
17430
- var branchActionSchema = import_zod58.z.discriminatedUnion("type", branchActionOptions);
17431
- var findOpportunitySchema = import_zod58.z.object({
17432
- type: import_zod58.z.literal("find_opportunity"),
17772
+ var branchActionSchema = import_zod59.z.discriminatedUnion("type", branchActionOptions);
17773
+ var findOpportunitySchema = import_zod59.z.object({
17774
+ type: import_zod59.z.literal("find_opportunity"),
17433
17775
  pipelineRef: nsRef("pipeline"),
17434
- found: import_zod58.z.array(branchActionSchema).default([]),
17435
- notFound: import_zod58.z.array(branchActionSchema).default([])
17776
+ found: import_zod59.z.array(branchActionSchema).default([]),
17777
+ notFound: import_zod59.z.array(branchActionSchema).default([])
17436
17778
  });
17437
- var actionSchema = import_zod58.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
17779
+ var actionSchema = import_zod59.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
17438
17780
  var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
17439
17781
  var CALL_STATUSES = ["busy", "canceled", "voicemail", "no-answer", "completed"];
17440
17782
  var NUMBER_VALIDATION_STATES = ["not_valid", "sms_incapable"];
17441
- var triggerSchema = import_zod58.z.object({
17442
- type: import_zod58.z.string(),
17783
+ var triggerSchema = import_zod59.z.object({
17784
+ type: import_zod59.z.string(),
17443
17785
  formRef: nsRef("form").optional(),
17444
17786
  tagRef: nsRef("tag").optional(),
17445
17787
  calendarRef: nsRef("calendar").optional(),
17446
17788
  pipelineRef: nsRef("pipeline").optional(),
17447
17789
  stageRef: nsRef("stage").optional(),
17448
17790
  // Required for a native `appointment` trigger (the status it fires on).
17449
- appointmentStatus: import_zod58.z.enum(APPOINTMENT_STATUSES).optional(),
17791
+ appointmentStatus: import_zod59.z.enum(APPOINTMENT_STATUSES).optional(),
17450
17792
  // ── customer_reply scoping ──────────────────────────────────────
17451
17793
  // A bare customer_reply trigger fires on EVERY inbound reply from EVERY
17452
17794
  // contact. That is almost never what a plan means: "alert the owner when a
@@ -17460,27 +17802,27 @@ var triggerSchema = import_zod58.z.object({
17460
17802
  // Both optional and additive — omitting them keeps the long-standing
17461
17803
  // fires-on-any-reply baseline.
17462
17804
  hasTagRef: nsRef("tag").optional(),
17463
- replyIntent: import_zod58.z.enum(["positive", "negative"]).optional(),
17805
+ replyIntent: import_zod59.z.enum(["positive", "negative"]).optional(),
17464
17806
  // ── call_status scoping (missed-call text-back) ─────────────────
17465
17807
  // GHL's call_status trigger fires on a completed call attempt. The states it
17466
17808
  // exposes are exactly what a "missed call" means operationally. Required for
17467
17809
  // a call_status trigger — without them the trigger would fire on EVERY call
17468
17810
  // including answered ones, so the executor refuses rather than guess.
17469
- callStatuses: import_zod58.z.array(import_zod58.z.enum(CALL_STATUSES)).optional(),
17470
- callDirection: import_zod58.z.enum(["inbound", "outbound"]).optional(),
17811
+ callStatuses: import_zod59.z.array(import_zod59.z.enum(CALL_STATUSES)).optional(),
17812
+ callDirection: import_zod59.z.enum(["inbound", "outbound"]).optional(),
17471
17813
  // ── validation_error (GHL UI: "Number validation") ──────────────
17472
17814
  // Fires after a phone number passes or fails a validation check. The UI shows
17473
17815
  // "Not valid" / "SMS incapable"; the WIRE values are snake_case. Required for
17474
17816
  // a validation_error trigger — without them the executor refuses rather than
17475
17817
  // guess which failure states to fire on.
17476
- numberValidation: import_zod58.z.array(import_zod58.z.enum(NUMBER_VALIDATION_STATES)).optional()
17818
+ numberValidation: import_zod59.z.array(import_zod59.z.enum(NUMBER_VALIDATION_STATES)).optional()
17477
17819
  });
17478
- var workflowSchema = import_zod58.z.object({
17820
+ var workflowSchema = import_zod59.z.object({
17479
17821
  ref: nsRef("workflow"),
17480
- name: import_zod58.z.string(),
17822
+ name: import_zod59.z.string(),
17481
17823
  trigger: triggerSchema.optional(),
17482
- stopOnResponse: import_zod58.z.boolean().optional(),
17483
- actions: import_zod58.z.array(actionSchema).max(40)
17824
+ stopOnResponse: import_zod59.z.boolean().optional(),
17825
+ actions: import_zod59.z.array(actionSchema).max(40)
17484
17826
  // house rule: <=40 actions/workflow
17485
17827
  });
17486
17828
  var HANDOFF_OWNER_LEGACY = {
@@ -17488,35 +17830,35 @@ var HANDOFF_OWNER_LEGACY = {
17488
17830
  "JERRY-EXT": "OPERATOR-EXT",
17489
17831
  "SASHA": "TEAM"
17490
17832
  };
17491
- var handoffSchema = import_zod58.z.object({
17833
+ var handoffSchema = import_zod59.z.object({
17492
17834
  ref: nsRef("handoff"),
17493
- owner: import_zod58.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
17494
- title: import_zod58.z.string(),
17495
- trigger: import_zod58.z.string().optional(),
17496
- instruction: import_zod58.z.string(),
17835
+ owner: import_zod59.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
17836
+ title: import_zod59.z.string(),
17837
+ trigger: import_zod59.z.string().optional(),
17838
+ instruction: import_zod59.z.string(),
17497
17839
  produces: refSchema.nullable().optional(),
17498
- successCheck: import_zod58.z.string(),
17499
- blocks: import_zod58.z.array(import_zod58.z.string()).optional()
17840
+ successCheck: import_zod59.z.string(),
17841
+ blocks: import_zod59.z.array(import_zod59.z.string()).optional()
17500
17842
  });
17501
- var buildPlanSchema = import_zod58.z.object({
17502
- schemaVersion: import_zod58.z.string(),
17503
- planId: import_zod58.z.string(),
17504
- briefId: import_zod58.z.string(),
17505
- preset: import_zod58.z.string(),
17506
- summary: import_zod58.z.string().optional(),
17507
- pipelines: import_zod58.z.array(pipelineSchema).optional(),
17508
- customFields: import_zod58.z.array(customFieldSchema).optional(),
17509
- tags: import_zod58.z.array(tagSchema).optional(),
17510
- customValues: import_zod58.z.array(customValueSchema).optional(),
17511
- calendars: import_zod58.z.array(calendarSchema).optional(),
17512
- forms: import_zod58.z.array(formSchema).optional(),
17513
- funnels: import_zod58.z.array(funnelSchema).optional(),
17514
- emails: import_zod58.z.array(emailAssetSchema).optional(),
17515
- sms: import_zod58.z.array(smsAssetSchema).optional(),
17516
- workflows: import_zod58.z.array(workflowSchema).optional(),
17517
- handoffs: import_zod58.z.array(handoffSchema).optional(),
17518
- buildOrder: import_zod58.z.array(import_zod58.z.string()).optional(),
17519
- idMap: import_zod58.z.record(import_zod58.z.string()).optional()
17843
+ var buildPlanSchema = import_zod59.z.object({
17844
+ schemaVersion: import_zod59.z.string(),
17845
+ planId: import_zod59.z.string(),
17846
+ briefId: import_zod59.z.string(),
17847
+ preset: import_zod59.z.string(),
17848
+ summary: import_zod59.z.string().optional(),
17849
+ pipelines: import_zod59.z.array(pipelineSchema).optional(),
17850
+ customFields: import_zod59.z.array(customFieldSchema).optional(),
17851
+ tags: import_zod59.z.array(tagSchema).optional(),
17852
+ customValues: import_zod59.z.array(customValueSchema).optional(),
17853
+ calendars: import_zod59.z.array(calendarSchema).optional(),
17854
+ forms: import_zod59.z.array(formSchema).optional(),
17855
+ funnels: import_zod59.z.array(funnelSchema).optional(),
17856
+ emails: import_zod59.z.array(emailAssetSchema).optional(),
17857
+ sms: import_zod59.z.array(smsAssetSchema).optional(),
17858
+ workflows: import_zod59.z.array(workflowSchema).optional(),
17859
+ handoffs: import_zod59.z.array(handoffSchema).optional(),
17860
+ buildOrder: import_zod59.z.array(import_zod59.z.string()).optional(),
17861
+ idMap: import_zod59.z.record(import_zod59.z.string()).optional()
17520
17862
  }).strict();
17521
17863
  function collectDefinedRefs(plan) {
17522
17864
  const refs = /* @__PURE__ */ new Map();
@@ -19186,16 +19528,16 @@ function msg(e) {
19186
19528
  }
19187
19529
 
19188
19530
  // src/tools/intake-to-build.ts
19189
- var customFieldItemSchema = import_zod59.z.object({
19190
- id: import_zod59.z.string(),
19191
- name: import_zod59.z.string(),
19192
- fieldKey: import_zod59.z.string(),
19193
- dataType: import_zod59.z.string(),
19194
- model: import_zod59.z.string().optional(),
19195
- parentId: import_zod59.z.string().optional(),
19196
- position: import_zod59.z.number().optional(),
19197
- dateAdded: import_zod59.z.string().optional(),
19198
- picklistOptions: import_zod59.z.array(import_zod59.z.string()).optional()
19531
+ var customFieldItemSchema = import_zod60.z.object({
19532
+ id: import_zod60.z.string(),
19533
+ name: import_zod60.z.string(),
19534
+ fieldKey: import_zod60.z.string(),
19535
+ dataType: import_zod60.z.string(),
19536
+ model: import_zod60.z.string().optional(),
19537
+ parentId: import_zod60.z.string().optional(),
19538
+ position: import_zod60.z.number().optional(),
19539
+ dateAdded: import_zod60.z.string().optional(),
19540
+ picklistOptions: import_zod60.z.array(import_zod60.z.string()).optional()
19199
19541
  }).passthrough();
19200
19542
  function parseCustomFields(raw) {
19201
19543
  const obj = raw && typeof raw === "object" ? raw : {};
@@ -19604,7 +19946,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19604
19946
  "validate_brief",
19605
19947
  "Validate an Intake-to-Build Brief object against the \xA74 schema. Returns {valid, errors}. Use to check a normalized brief before handing it to the plan-generation skill.",
19606
19948
  {
19607
- brief: import_zod59.z.record(import_zod59.z.unknown()).describe("The Brief object to validate.")
19949
+ brief: import_zod60.z.record(import_zod60.z.unknown()).describe("The Brief object to validate.")
19608
19950
  },
19609
19951
  async ({ brief }) => validateBrief(brief)
19610
19952
  );
@@ -19613,7 +19955,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19613
19955
  "validate_build_plan",
19614
19956
  "Validate an Intake-to-Build Build Plan against the \xA75 schema AND check ref-integrity: every symbolic ref (pipeline.*, stage.*, tag.*, ...) must resolve to a defined object of the right type, and refs must be unique. Dead references are reported as errors here, before any build runs \u2014 the structural guard against 'an invalid ID silently kills downstream actions'. Returns {valid, errors, warnings, referencesScanned}.",
19615
19957
  {
19616
- plan: import_zod59.z.record(import_zod59.z.unknown()).describe("The Build Plan object to validate.")
19958
+ plan: import_zod60.z.record(import_zod60.z.unknown()).describe("The Build Plan object to validate.")
19617
19959
  },
19618
19960
  async ({ plan }) => validateBuildPlan(plan)
19619
19961
  );
@@ -19621,12 +19963,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19621
19963
  "apply_build_plan",
19622
19964
  `Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values), calendars, AND forms: never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they're surfaced as a manual step, not auto-staffed to a guess. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). Funnels: a GHL funnel (target:"ghl", default) builds structurally (funnel + named steps; page content/HTML is a manual step \u2014 plans carry outlines); a funnel with target:"external" is NOT built or deployed here \u2014 the user builds + hosts the site themselves (Cloudflare/Vercel) and wires its form back to this GHL sub-account (surfaced as a manual wiring step). Workflows build as DRAFT with all their logical actions expanded to native GHL JSON (incl. opportunity create/move steps, re-enabled v3.41.0) and chained; a contact_tag trigger is built automatically, other trigger types are surfaced as a manual step; the operator reviews + publishes. Always confirms the active location and validates the plan before any write.`,
19623
19965
  {
19624
- plan: import_zod59.z.record(import_zod59.z.unknown()).describe("The approved \xA75 Build Plan object."),
19625
- mode: import_zod59.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
19626
- locationId: import_zod59.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
19627
- metHandoffs: import_zod59.z.array(import_zod59.z.string()).optional().describe('Handoff refs the operator has already satisfied (e.g. ["handoff.a2p"]) \u2014 lifts their gate so dependent workflows are not held DRAFT.'),
19628
- publishWorkflows: import_zod59.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
19629
- onConflict: import_zod59.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
19966
+ plan: import_zod60.z.record(import_zod60.z.unknown()).describe("The approved \xA75 Build Plan object."),
19967
+ mode: import_zod60.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
19968
+ locationId: import_zod60.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
19969
+ metHandoffs: import_zod60.z.array(import_zod60.z.string()).optional().describe('Handoff refs the operator has already satisfied (e.g. ["handoff.a2p"]) \u2014 lifts their gate so dependent workflows are not held DRAFT.'),
19970
+ publishWorkflows: import_zod60.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
19971
+ onConflict: import_zod60.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
19630
19972
  },
19631
19973
  async ({ plan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
19632
19974
  try {
@@ -19829,9 +20171,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19829
20171
  "install_intake_form",
19830
20172
  "Install the canonical Intake-to-Build client-intake form into the CURRENT GHL location (whatever get_current_location returns). Account-agnostic, zero hardcoded IDs. Creates any missing intake custom fields (idempotent \u2014 reused on re-run), then builds the form with the proven GHL form-builder field shapes and verifies it. Returns {formId, fieldMap} \u2014 keep fieldMap; normalize_submission_to_brief uses it. Pass dryRun:true to preview what would be created without writing. Pass formId to update an existing intake form in place instead of creating a new one.",
19831
20173
  {
19832
- dryRun: import_zod59.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
19833
- formId: import_zod59.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
19834
- formName: import_zod59.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
20174
+ dryRun: import_zod60.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
20175
+ formId: import_zod60.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
20176
+ formName: import_zod60.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`)
19835
20177
  },
19836
20178
  async ({ dryRun, formId, formName }) => {
19837
20179
  try {
@@ -19919,10 +20261,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19919
20261
  "normalize_submission_to_brief",
19920
20262
  'Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). Pass the formId; by default the most recent submission is used (or pass submissionId). The intakeKey->customFieldId map is taken from fieldMap if provided (the install_intake_form output, most robust), otherwise reconstructed from the live form. Returns {brief, validation, submissionId} \u2014 validation flags any missing required fields (e.g. an incomplete submission).',
19921
20263
  {
19922
- formId: import_zod59.z.string().describe("The intake form ID (from install_intake_form)."),
19923
- submissionId: import_zod59.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
19924
- fieldMap: import_zod59.z.record(import_zod59.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
19925
- preset: import_zod59.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
20264
+ formId: import_zod60.z.string().describe("The intake form ID (from install_intake_form)."),
20265
+ submissionId: import_zod60.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
20266
+ fieldMap: import_zod60.z.record(import_zod60.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
20267
+ preset: import_zod60.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
19926
20268
  },
19927
20269
  async ({ formId, submissionId, fieldMap, preset }) => {
19928
20270
  try {
@@ -19951,7 +20293,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
19951
20293
  const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
19952
20294
  resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
19953
20295
  }
19954
- const presetSchema2 = import_zod59.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
20296
+ const presetSchema2 = import_zod60.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
19955
20297
  const presetParsed = presetSchema2.safeParse(preset);
19956
20298
  const brief = normalizeSubmissionToBrief({
19957
20299
  others,
@@ -20033,6 +20375,7 @@ var FORM_BUILDER_MODULE = "form-builder";
20033
20375
  var INTAKE_TO_BUILD_MODULE = "intake-to-build";
20034
20376
  var FUNNEL_QA_MODULE = "funnel-qa";
20035
20377
  var USER_GUIDE_MODULE = "user-guide";
20378
+ var CHECKUP_MODULE = "checkup";
20036
20379
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
20037
20380
  ...publicApiTools.map(([, label]) => label),
20038
20381
  ...internalApiTools.map(([, label]) => label),
@@ -20040,6 +20383,7 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
20040
20383
  INTAKE_TO_BUILD_MODULE,
20041
20384
  FUNNEL_QA_MODULE,
20042
20385
  USER_GUIDE_MODULE,
20386
+ CHECKUP_MODULE,
20043
20387
  VALIDATORS_MODULE,
20044
20388
  DIAGNOSTICS_MODULE,
20045
20389
  LOCATION_SWITCHER_MODULE,
@@ -20072,6 +20416,14 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
20072
20416
  registry2 ?? null,
20073
20417
  () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
20074
20418
  );
20419
+ registerCheckupTools(
20420
+ wrap(CHECKUP_MODULE),
20421
+ mcpVersion ?? "unknown",
20422
+ client,
20423
+ builderClient,
20424
+ registry2 ?? null,
20425
+ () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
20426
+ );
20075
20427
  registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
20076
20428
  registerAccountExportTools(wrap(ACCOUNT_EXPORT_MODULE), client, registry2, builderClient);
20077
20429
  registerLocationSwitcherTools(
@@ -20129,7 +20481,7 @@ init_credentials_store();
20129
20481
  init_setup_tool();
20130
20482
 
20131
20483
  // src/tools/skills.ts
20132
- var import_zod60 = require("zod");
20484
+ var import_zod61 = require("zod");
20133
20485
 
20134
20486
  // src/skill-installer.ts
20135
20487
  var import_node_crypto2 = require("node:crypto");
@@ -20249,7 +20601,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
20249
20601
  "install_skills",
20250
20602
  "Install (or repair) the guided skills bundled with GHL Command \u2014 Blueprint (build a whole client account from one intake), Clone Site (clone and rebrand a live web page for a client, with a rights declaration and a pre-launch liability report), and GHL Reports (verified counts, lists, and weekly reports without token burn) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
20251
20603
  {
20252
- targetDir: import_zod60.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
20604
+ targetDir: import_zod61.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
20253
20605
  },
20254
20606
  async ({ targetDir }) => {
20255
20607
  try {