@elitedcs/ghl-mcp 3.83.1 → 3.83.3

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
@@ -241,7 +241,7 @@ function foldCredentialsIntoEnv(creds, env = process.env) {
241
241
  ];
242
242
  for (const [credKey, envKey] of mappings) {
243
243
  const value = creds[credKey];
244
- if (value && !env[envKey]) {
244
+ if (typeof value === "string" && value && !env[envKey]) {
245
245
  env[envKey] = value;
246
246
  }
247
247
  }
@@ -740,13 +740,13 @@ var init_token_registry = __esm({
740
740
  name: import_zod2.z.string().optional()
741
741
  });
742
742
  TokenRegistryDataSchema = import_zod2.z.object({
743
- tokens: import_zod2.z.record(LocationTokenSchema),
743
+ tokens: import_zod2.z.record(import_zod2.z.string(), LocationTokenSchema),
744
744
  agencyKey: import_zod2.z.string().optional(),
745
745
  // "Home" Firebase — the company the install was set up for (credentials.json).
746
746
  firebase: FirebaseConfigSchema.optional(),
747
747
  // Additional companies' Firebase auth, keyed by GHL companyId. Lets ONE
748
748
  // process operate the workflow builder across multiple clients' GHL accounts.
749
- firebaseByCompany: import_zod2.z.record(CompanyFirebaseSchema).optional()
749
+ firebaseByCompany: import_zod2.z.record(import_zod2.z.string(), CompanyFirebaseSchema).optional()
750
750
  });
751
751
  TokenRegistry = class _TokenRegistry {
752
752
  data;
@@ -1256,7 +1256,7 @@ var init_trigger_schemas = __esm({
1256
1256
  masterType: import_zod3.z.string().optional(),
1257
1257
  name: import_zod3.z.string().optional(),
1258
1258
  actions: import_zod3.z.array(TriggerActionSchema).optional(),
1259
- schedule_config: import_zod3.z.record(import_zod3.z.unknown()).optional(),
1259
+ schedule_config: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).optional(),
1260
1260
  date_updated: import_zod3.z.string().optional()
1261
1261
  }).passthrough();
1262
1262
  ContactTagTriggerSchema = TriggerCommonSchema.extend({
@@ -1547,7 +1547,7 @@ var init_trigger_schemas = __esm({
1547
1547
  });
1548
1548
  UnknownTriggerSchema = TriggerCommonSchema.extend({
1549
1549
  type: import_zod3.z.string(),
1550
- conditions: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())).optional()
1550
+ conditions: import_zod3.z.array(import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown())).optional()
1551
1551
  });
1552
1552
  WorkflowTriggerSchema = import_zod3.z.union([
1553
1553
  ContactTagTriggerSchema,
@@ -3883,7 +3883,8 @@ function annotateListResponse(raw, listKey, limit, nextHint) {
3883
3883
  };
3884
3884
  }
3885
3885
  function safeTool(server2, name, description, schema, handler) {
3886
- server2.tool(name, description, schema, async (args) => {
3886
+ const toolServer = server2;
3887
+ toolServer.tool(name, description, schema, async (args) => {
3887
3888
  try {
3888
3889
  return jsonResponse(await handler(args));
3889
3890
  } catch (error) {
@@ -5090,7 +5091,7 @@ function registerCalendarTools(server2, client) {
5090
5091
  slug: import_zod12.z.string().optional().describe("Calendar slug"),
5091
5092
  widgetSlug: import_zod12.z.string().optional().describe("Widget slug for embedding"),
5092
5093
  calendarType: import_zod12.z.string().optional().describe("Calendar type"),
5093
- teamMembers: import_zod12.z.array(import_zod12.z.record(import_zod12.z.unknown())).optional().describe("Array of team member objects"),
5094
+ teamMembers: import_zod12.z.array(import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())).optional().describe("Array of team member objects"),
5094
5095
  eventTitle: import_zod12.z.string().optional().describe("Default event title"),
5095
5096
  eventColor: import_zod12.z.string().optional().describe("Event color hex code"),
5096
5097
  slotDuration: import_zod12.z.number().optional().describe("Slot duration in minutes"),
@@ -5386,7 +5387,7 @@ function registerLocationTools(server2, client) {
5386
5387
  timezone: import_zod13.z.string().optional().describe("IANA timezone (e.g., 'America/New_York')."),
5387
5388
  phone: import_zod13.z.string().optional().describe("Phone number."),
5388
5389
  email: import_zod13.z.string().optional().describe("Email address."),
5389
- settings: import_zod13.z.record(import_zod13.z.unknown()).optional().describe("Location settings object with key-value pairs."),
5390
+ settings: import_zod13.z.record(import_zod13.z.string(), import_zod13.z.unknown()).optional().describe("Location settings object with key-value pairs."),
5390
5391
  overwriteBusinessInfo: import_zod13.z.boolean().optional().describe(
5391
5392
  "Allow a CHANGE to business details on a sub-account that already has a registered A2P brand. Off by default. Only needed when the account has a brandId AND you are changing (not filling) one of name/address/city/state/postalCode/country/phone/website/email."
5392
5393
  )
@@ -6726,7 +6727,7 @@ function registerUserTools(server2, client, registry2) {
6726
6727
  role: import_zod22.z.enum(["admin", "user"]).describe("GHL role on this sub-account. 'admin' = full settings access; 'user' = day-to-day staff (contacts, conversations, calendar, opportunities)."),
6727
6728
  phone: import_zod22.z.string().optional().describe("Phone number (E.164 preferred)."),
6728
6729
  password: import_zod22.z.string().min(8).optional().describe("Only if you must set one. Omit it (recommended) so GHL sends the invite instead."),
6729
- permissions: import_zod22.z.record(import_zod22.z.boolean()).optional().describe("Override the default per-role permissions object entirely (GHL UserPermissions booleans)."),
6730
+ permissions: import_zod22.z.record(import_zod22.z.string(), import_zod22.z.boolean()).optional().describe("Override the default per-role permissions object entirely (GHL UserPermissions booleans)."),
6730
6731
  companyId: import_zod22.z.string().optional().describe("Agency/company id. Resolved from the registered location by default; pass it only when that lookup fails."),
6731
6732
  locationId: import_zod22.z.string().optional().describe("Sub-account to add the user to (defaults to the active location). The user is granted access to this ONE location.")
6732
6733
  },
@@ -7432,10 +7433,11 @@ function registerEmailTools(server2, client) {
7432
7433
  );
7433
7434
  }
7434
7435
  function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
7436
+ if (!builderClient) return;
7435
7437
  const client = builderClient;
7436
- if (!client) return;
7437
7438
  async function builderRequest(method, path39, body) {
7438
7439
  const headers = await client.buildHeaders();
7440
+ assertWriteAllowed(method, `${EMAIL_BUILDER_BASE}${path39}`, { defaultLocationId: client.locationId });
7439
7441
  const response = await fetch(`${EMAIL_BUILDER_BASE}${path39}`, {
7440
7442
  method,
7441
7443
  headers,
@@ -7541,6 +7543,7 @@ var init_emails = __esm({
7541
7543
  init_account_reads();
7542
7544
  import_zod28 = require("zod");
7543
7545
  init_tool_helpers();
7546
+ init_account_rules();
7544
7547
  EMAIL_BUILDER_BASE = "https://backend.leadconnectorhq.com/emails/builder";
7545
7548
  SNIPPETS_BASE = "https://services.leadconnectorhq.com/snippets";
7546
7549
  TEMPLATE_TYPES = ["html", "folder", "import", "builder", "blank", "ai_template", "vibe-editor"];
@@ -7712,7 +7715,7 @@ function registerCustomObjectTools(server2, client) {
7712
7715
  {
7713
7716
  schemaKey: import_zod30.z.string().describe("The custom object schema key."),
7714
7717
  locationId: import_zod30.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
7715
- properties: import_zod30.z.record(import_zod30.z.unknown()).describe("Key-value pairs of field values for the new record.")
7718
+ properties: import_zod30.z.record(import_zod30.z.string(), import_zod30.z.unknown()).describe("Key-value pairs of field values for the new record.")
7716
7719
  },
7717
7720
  async ({ schemaKey, locationId: locationId2, properties }) => {
7718
7721
  const resolvedLocationId = client.resolveLocationId(locationId2);
@@ -7729,7 +7732,7 @@ function registerCustomObjectTools(server2, client) {
7729
7732
  schemaKey: import_zod30.z.string().describe("The custom object schema key."),
7730
7733
  recordId: import_zod30.z.string().describe("The record ID to update."),
7731
7734
  locationId: import_zod30.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
7732
- properties: import_zod30.z.record(import_zod30.z.unknown()).describe("Key-value pairs of field values to update.")
7735
+ properties: import_zod30.z.record(import_zod30.z.string(), import_zod30.z.unknown()).describe("Key-value pairs of field values to update.")
7733
7736
  },
7734
7737
  async ({ schemaKey, recordId, locationId: locationId2, properties }) => {
7735
7738
  const resolvedLocationId = client.resolveLocationId(locationId2);
@@ -7990,7 +7993,7 @@ function registerEstimateTools(server2, client) {
7990
7993
  price: import_zod32.z.number().describe("Unit price, in the estimate currency."),
7991
7994
  currency: import_zod32.z.string().optional().describe("Currency code (e.g. USD).")
7992
7995
  })).optional().describe("Updated line items. Give quantity and price; they are translated to the qty/amount/type GHL requires."),
7993
- discount: import_zod32.z.record(import_zod32.z.unknown()).optional().describe("Updated discount object.")
7996
+ discount: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.unknown()).optional().describe("Updated discount object.")
7994
7997
  },
7995
7998
  async ({ estimateId, locationId: locationId2, name, title: title2, items, discount }) => {
7996
7999
  const resolvedLocationId = client.resolveLocationId(locationId2);
@@ -8769,7 +8772,7 @@ function registerConversationAiTools(server2, client) {
8769
8772
  agentId: import_zod35.z.string().describe("Agent id."),
8770
8773
  type: import_zod35.z.enum(ACTION_TYPES),
8771
8774
  name: import_zod35.z.string().min(1).describe("Name the operator sees."),
8772
- details: import_zod35.z.record(import_zod35.z.unknown()).describe("Type-specific details. humanHandOver: enabled, triggerCondition, handoverType (contactRequest|lackOfInformation|failedToResolveIssue|custom), examples[], assignToUserId, finalMessage, reactivateEnabled(+sleepTime/Unit). appointmentBooking: calendarId, onlySendLink, triggerWorkflow(+workflowIds), sleepAfterBooking(+sleepTime/Unit), transferBot(+transferAgent), rescheduleEnabled, cancelEnabled. triggerWorkflow: workflowIds[], triggerCondition, triggerMessage. stopBot: stopBotDetectionType (Goodbye|Custom), stopBotTriggerCondition, stopBotExamples[] (2+), finalMessage, enabled, reactivateEnabled, tags[]. updateContactField: contactFieldId, description, contactUpdateExamples[]. transferBot: transferBotType, transferToBot, enabled. advancedFollowup: enabled, scenarioId, followupSequence[].")
8775
+ details: import_zod35.z.record(import_zod35.z.string(), import_zod35.z.unknown()).describe("Type-specific details. humanHandOver: enabled, triggerCondition, handoverType (contactRequest|lackOfInformation|failedToResolveIssue|custom), examples[], assignToUserId, finalMessage, reactivateEnabled(+sleepTime/Unit). appointmentBooking: calendarId, onlySendLink, triggerWorkflow(+workflowIds), sleepAfterBooking(+sleepTime/Unit), transferBot(+transferAgent), rescheduleEnabled, cancelEnabled. triggerWorkflow: workflowIds[], triggerCondition, triggerMessage. stopBot: stopBotDetectionType (Goodbye|Custom), stopBotTriggerCondition, stopBotExamples[] (2+), finalMessage, enabled, reactivateEnabled, tags[]. updateContactField: contactFieldId, description, contactUpdateExamples[]. transferBot: transferBotType, transferToBot, enabled. advancedFollowup: enabled, scenarioId, followupSequence[].")
8773
8776
  },
8774
8777
  async ({ agentId, type, name, details }) => {
8775
8778
  const problem = actionBodyProblem(type, details);
@@ -8787,7 +8790,7 @@ function registerConversationAiTools(server2, client) {
8787
8790
  actionId: import_zod35.z.string().describe("Action id."),
8788
8791
  type: import_zod35.z.enum(ACTION_TYPES),
8789
8792
  name: import_zod35.z.string().min(1),
8790
- details: import_zod35.z.record(import_zod35.z.unknown()).describe("The complete details for this type.")
8793
+ details: import_zod35.z.record(import_zod35.z.string(), import_zod35.z.unknown()).describe("The complete details for this type.")
8791
8794
  },
8792
8795
  async ({ agentId, actionId, type, name, details }) => {
8793
8796
  const problem = actionBodyProblem(type, details);
@@ -10024,7 +10027,7 @@ function registerVoiceAiTools(server2, shared) {
10024
10027
  locationId: LOCATION,
10025
10028
  actionType: import_zod37.z.enum(VOICE_ACTION_TYPES),
10026
10029
  name: import_zod37.z.string().min(1).describe("Name the operator sees. Letters, numbers, spaces, _ and - only."),
10027
- actionParameters: import_zod37.z.record(import_zod37.z.unknown()).describe(ACTION_PARAMS_DOC)
10030
+ actionParameters: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe(ACTION_PARAMS_DOC)
10028
10031
  },
10029
10032
  async ({ agentId, locationId: locationId2, actionType, name, actionParameters }) => {
10030
10033
  const client = shared.pinned();
@@ -10060,7 +10063,7 @@ function registerVoiceAiTools(server2, shared) {
10060
10063
  locationId: LOCATION,
10061
10064
  actionType: import_zod37.z.enum(VOICE_ACTION_TYPES),
10062
10065
  name: import_zod37.z.string().min(1),
10063
- actionParameters: import_zod37.z.record(import_zod37.z.unknown()).describe("The complete parameters for this type.")
10066
+ actionParameters: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("The complete parameters for this type.")
10064
10067
  },
10065
10068
  async ({ agentId, actionId, locationId: locationId2, actionType, name, actionParameters }) => {
10066
10069
  const client = shared.pinned();
@@ -10288,12 +10291,15 @@ function readId(workflow) {
10288
10291
  const id = workflow._id ?? record.id;
10289
10292
  return typeof id === "string" ? id : "";
10290
10293
  }
10294
+ function attributeBag(attributes) {
10295
+ return attributes ? Object.fromEntries(Object.entries(attributes)) : {};
10296
+ }
10291
10297
  function summarizeWebhookWorkflow(workflow) {
10292
10298
  const templates = workflow.workflowData?.templates ?? [];
10293
10299
  const webhookActions = templates.filter((action) => action.type === "webhook");
10294
10300
  if (!webhookActions.length) return null;
10295
10301
  const actions = webhookActions.map((action) => {
10296
- const attributes = action.attributes ?? {};
10302
+ const attributes = attributeBag(action.attributes);
10297
10303
  return {
10298
10304
  actionId: action.id,
10299
10305
  name: action.name,
@@ -10450,7 +10456,7 @@ function registerWebhookTools(server2, builderClient) {
10450
10456
  }
10451
10457
  const target = actionId ? webhookActions.find((a) => a.id === actionId) : webhookActions[0];
10452
10458
  if (!target) throw new Error(`No webhook action with id ${actionId} on workflow ${webhookId}.`);
10453
- const attributes = target.attributes ?? {};
10459
+ const attributes = attributeBag(target.attributes);
10454
10460
  const validated = buildWebhookAction({
10455
10461
  id: target.id,
10456
10462
  name: target.name,
@@ -10522,7 +10528,7 @@ var init_webhooks = __esm({
10522
10528
  import_zod38.z.string().describe("A GHL trigger type, e.g. 'contact_created'. Fires with no filter."),
10523
10529
  import_zod38.z.object({
10524
10530
  type: import_zod38.z.string().describe("A GHL trigger type, e.g. 'contact_tag' or 'pipeline_stage_updated'."),
10525
- conditions: import_zod38.z.array(import_zod38.z.record(import_zod38.z.unknown())).optional().describe(
10531
+ conditions: import_zod38.z.array(import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown())).optional().describe(
10526
10532
  "Filter conditions, exactly as update_workflow_actions takes them. Use get_trigger_registry to find the valid fields for a trigger type. Omit to fire on every event of that type."
10527
10533
  )
10528
10534
  })
@@ -10607,8 +10613,8 @@ function registerDocumentTools(server2, client) {
10607
10613
  sentBy: import_zod39.z.string().optional().describe("GHL user ID of the sender (required by the API; falls back to GHL_USER_ID env)."),
10608
10614
  documentName: import_zod39.z.string().optional().describe("Override the document name shown to recipients."),
10609
10615
  medium: import_zod39.z.string().optional().describe("Delivery medium, e.g. 'email'. Defaults to email."),
10610
- ccRecipients: import_zod39.z.array(import_zod39.z.record(import_zod39.z.unknown())).optional().describe("Optional CC recipients: [{id, email, contactName, firstName, lastName, imageUrl}]."),
10611
- notificationSettings: import_zod39.z.record(import_zod39.z.unknown()).optional().describe("Optional sender/receiver notification settings: {sender:{fromName,fromEmail}, receive:{subject,templateId}}."),
10616
+ ccRecipients: import_zod39.z.array(import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown())).optional().describe("Optional CC recipients: [{id, email, contactName, firstName, lastName, imageUrl}]."),
10617
+ notificationSettings: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()).optional().describe("Optional sender/receiver notification settings: {sender:{fromName,fromEmail}, receive:{subject,templateId}}."),
10612
10618
  locationId: import_zod39.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
10613
10619
  },
10614
10620
  async ({ documentId, sentBy, documentName, medium, ccRecipients, notificationSettings, locationId: locationId2 }) => {
@@ -11055,7 +11061,7 @@ function registerWorkflowBuilderTools(server2, client) {
11055
11061
  "invoice_paid",
11056
11062
  "review_request_clicked"
11057
11063
  ]).describe("The goal-condition identifier. Extras shape per condition: email_event \u2192 {stepIds:[]}; link_click \u2192 {linkIds:[]}; appointment_status \u2192 {calendarId}; payment_received \u2192 {globalProductIds:[]}; form_submission \u2192 {formIds:[]}; document_status \u2192 {templateId}; invoice_paid \u2192 {invoiceStepId}; review_request_clicked \u2192 {reviewTypes:['sms'|'email'], reviewLinkId}; add_contact_tag/remove_contact_tag \u2192 {tags:[]} (uncaptured but inferred)."),
11058
- extras: import_zod40.z.record(import_zod40.z.unknown()).optional().describe("Goal-condition-specific config. See goal_condition description for the expected shape per value. Pass {} or omit for goal_conditions without extras."),
11064
+ extras: import_zod40.z.record(import_zod40.z.string(), import_zod40.z.unknown()).optional().describe("Goal-condition-specific config. See goal_condition description for the expected shape per value. Pass {} or omit for goal_conditions without extras."),
11059
11065
  action: import_zod40.z.enum(["exit", "continue", "wait"]).default("exit").describe("What happens when the goal fires (per GHL's GoalAction enum). 'exit' terminates the workflow path (verified). 'continue' lets it proceed to a downstream node. 'wait' pauses on the goal."),
11060
11066
  name: import_zod40.z.string().default("Goal").describe("Display name in the GHL UI."),
11061
11067
  op: import_zod40.z.enum(["or", "and"]).default("or").describe("Top-level boolean operator across segments. Default 'or'."),
@@ -11134,7 +11140,7 @@ var init_workflow_builder = __esm({
11134
11140
  id: import_zod40.z.string().optional().describe("Action ID (auto-generated if omitted)."),
11135
11141
  name: import_zod40.z.string().describe("Display name for this action step."),
11136
11142
  type: import_zod40.z.string().describe("Action type."),
11137
- attributes: import_zod40.z.record(import_zod40.z.unknown()).optional().describe("Action-specific configuration."),
11143
+ attributes: import_zod40.z.record(import_zod40.z.string(), import_zod40.z.unknown()).optional().describe("Action-specific configuration."),
11138
11144
  next: import_zod40.z.union([import_zod40.z.string(), import_zod40.z.array(import_zod40.z.string())]).optional().describe("Next action id, or branch id array for condition nodes."),
11139
11145
  parent: import_zod40.z.string().optional().describe("Branch parent node id."),
11140
11146
  parentKey: import_zod40.z.string().optional().describe("Previous action id or branch parent id."),
@@ -11190,8 +11196,7 @@ var init_page_reads = __esm({
11190
11196
 
11191
11197
  // src/tools/funnel-builder.ts
11192
11198
  function registerFunnelBuilderTools(server2, builderClient) {
11193
- const client = builderClient;
11194
- if (!client) {
11199
+ if (!builderClient) {
11195
11200
  server2.tool(
11196
11201
  "funnel_builder_status",
11197
11202
  "Check whether the funnel / page builder is configured. It needs Firebase credentials, added via enable_workflow_builder.",
@@ -11219,6 +11224,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
11219
11224
  );
11220
11225
  return;
11221
11226
  }
11227
+ const client = builderClient;
11222
11228
  async function internalGet(path39) {
11223
11229
  return client.request("GET", path39);
11224
11230
  }
@@ -11506,6 +11512,7 @@ ${text2}`);
11506
11512
  headers.Origin = "https://page-builder.leadconnectorhq.com";
11507
11513
  headers.Referer = "https://page-builder.leadconnectorhq.com/";
11508
11514
  const url = `https://backend.leadconnectorhq.com/funnels/builder/autosave/${pageId}`;
11515
+ assertWriteAllowed("POST", url, { defaultLocationId: client.locationId });
11509
11516
  const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
11510
11517
  if (!response.ok) {
11511
11518
  throw new Error(`Funnel API Error ${response.status}: POST /builder/autosave/${pageId}
@@ -11524,7 +11531,7 @@ ${await response.text()}`);
11524
11531
  "Update a funnel/website page's builder content \u2014 sections, elements, settings, pageStyles, trackingCode, popups, popupsList, fontsForPreview. Use get_page_content first to see the current structure, modify it, then pass the full envelope here. Writes via GHL's builder autosave and returns the new pageDataUrl. \u26A0\uFE0F GHL accepts a save onto a page that already has content but may never make it the visible version (live-tested 2026-07-28 and again 2026-08-31). The tool now VERIFIES this for you: every response carries `save_check`, and `save_check.visible: false` means the content was stored but visitors still see the previous page \u2014 rebuild on a fresh step (create_funnel_page) and save onto that. Never report a page as updated without reading save_check. Saves as a draft unless isPublished is true.",
11525
11532
  {
11526
11533
  pageId: import_zod41.z.string().describe("The page ID to update."),
11527
- content: import_zod41.z.record(import_zod41.z.unknown()).describe("The full page content JSON \u2014 the 8-field envelope: sections, settings, general, pageStyles, trackingCode, fontsForPreview, popups, popupsList. Start from get_page_content and modify it."),
11534
+ content: import_zod41.z.record(import_zod41.z.string(), import_zod41.z.unknown()).describe("The full page content JSON \u2014 the 8-field envelope: sections, settings, general, pageStyles, trackingCode, fontsForPreview, popups, popupsList. Start from get_page_content and modify it."),
11528
11535
  funnelId: import_zod41.z.string().optional().describe("Funnel ID that owns the page. Optional \u2014 auto-resolved from the page metadata if omitted."),
11529
11536
  isPublished: import_zod41.z.boolean().optional().describe("Mark the page published after the save. Defaults to false (draft).")
11530
11537
  },
@@ -11614,6 +11621,7 @@ var init_funnel_builder = __esm({
11614
11621
  import_zod41 = require("zod");
11615
11622
  init_tool_helpers();
11616
11623
  init_page_reads();
11624
+ init_account_rules();
11617
11625
  }
11618
11626
  });
11619
11627
 
@@ -11814,6 +11822,9 @@ function requireCompany(api) {
11814
11822
  }
11815
11823
  async function call(api, path39, init) {
11816
11824
  const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
11825
+ if (MUTATING_PATHS.some((mutating) => path39.startsWith(mutating))) {
11826
+ assertWriteAllowed("POST", SVC + path39, { defaultLocationId: api.locationId() });
11827
+ }
11817
11828
  const doFetch = api.fetch ?? fetch;
11818
11829
  const response = await doFetch(SVC + path39, { ...init, headers });
11819
11830
  const text = await response.text();
@@ -11907,12 +11918,14 @@ function rankForBrand(rows, brand) {
11907
11918
  return { ...t, why: parts.join("; ") };
11908
11919
  });
11909
11920
  }
11910
- var SVC, GHL_ID;
11921
+ var SVC, MUTATING_PATHS, GHL_ID;
11911
11922
  var init_ghl_templates = __esm({
11912
11923
  "src/command-os/design/ghl-templates.ts"() {
11913
11924
  "use strict";
11914
11925
  init_color();
11926
+ init_account_rules();
11915
11927
  SVC = "https://services.leadconnectorhq.com";
11928
+ MUTATING_PATHS = ["/templates/template/load"];
11916
11929
  GHL_ID = /^[A-Za-z0-9]{15,40}$/;
11917
11930
  }
11918
11931
  });
@@ -16437,8 +16450,8 @@ function removalHoldBack(opts) {
16437
16450
  return null;
16438
16451
  }
16439
16452
  function registerPageStudioTools(server2, builderClient) {
16453
+ if (!builderClient) return;
16440
16454
  const client = builderClient;
16441
- if (!client) return;
16442
16455
  const templateApi = () => ({
16443
16456
  headers: () => client.buildHeaders(),
16444
16457
  companyId: () => client.getIntendedCompanyId() ?? client.getCurrentCompanyId(),
@@ -16487,7 +16500,9 @@ ${await response.text()}`);
16487
16500
  const headers = await client.buildHeaders();
16488
16501
  headers.Origin = "https://page-builder.leadconnectorhq.com";
16489
16502
  headers.Referer = "https://page-builder.leadconnectorhq.com/";
16490
- const response = await fetch(`https://backend.leadconnectorhq.com/funnels/builder/autosave/${pageId}`, {
16503
+ const autosaveUrl = `https://backend.leadconnectorhq.com/funnels/builder/autosave/${pageId}`;
16504
+ assertWriteAllowed("POST", autosaveUrl, { defaultLocationId: client.locationId });
16505
+ const response = await fetch(autosaveUrl, {
16491
16506
  method: "POST",
16492
16507
  headers,
16493
16508
  body: JSON.stringify(body)
@@ -16984,6 +16999,7 @@ var init_page_studio = __esm({
16984
16999
  init_tool_helpers();
16985
17000
  init_compose();
16986
17001
  init_website();
17002
+ init_account_rules();
16987
17003
  valueCardSchema = import_zod43.z.object({
16988
17004
  title: import_zod43.z.string().describe("Card title (short, uppercase reads best)."),
16989
17005
  bodyHtml: import_zod43.z.string().describe("Card body as HTML (<p>\u2026</p>). May use {{location.*}} merge fields."),
@@ -17074,6 +17090,7 @@ function cleanFormDataForSave(formData) {
17074
17090
  async function formApiRequest(client, method, path39, body) {
17075
17091
  const headers = await client.buildHeaders();
17076
17092
  const url = `https://backend.leadconnectorhq.com/forms${path39}`;
17093
+ assertWriteAllowed(method, url, { defaultLocationId: client.locationId });
17077
17094
  const options = { method, headers };
17078
17095
  if (body && (method === "POST" || method === "PUT")) {
17079
17096
  options.body = JSON.stringify(body);
@@ -17148,7 +17165,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17148
17165
  "Update a form's structure \u2014 fields, labels, conditional logic, auto-responder, email notifications, styling. Use get_form_full first to read the current form, modify the formData object, then pass it here. If name is omitted, the current name is preserved (one extra GET).",
17149
17166
  {
17150
17167
  formId: import_zod44.z.string().describe("The form ID to update."),
17151
- formData: import_zod44.z.record(import_zod44.z.unknown()).describe("The updated formData object containing form fields, settings, autoResponder config, etc."),
17168
+ formData: import_zod44.z.record(import_zod44.z.string(), import_zod44.z.unknown()).describe("The updated formData object containing form fields, settings, autoResponder config, etc."),
17152
17169
  name: import_zod44.z.string().optional().describe("Updated form name. If omitted, the current name is preserved.")
17153
17170
  },
17154
17171
  async ({ formId, formData, name }) => {
@@ -17276,6 +17293,7 @@ var init_form_builder = __esm({
17276
17293
  "use strict";
17277
17294
  import_zod44 = require("zod");
17278
17295
  init_tool_helpers();
17296
+ init_account_rules();
17279
17297
  }
17280
17298
  });
17281
17299
 
@@ -17407,7 +17425,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
17407
17425
  funnelUrl: import_zod45.z.string().describe("The live URL the test submission POSTs to (your external lead-bridge Worker URL, or any endpoint that creates the GHL contact). POSTed as JSON."),
17408
17426
  sentinelEmail: import_zod45.z.string().optional().describe("Override the minted sentinel email. Default: a unique blueprint-qa.<id>@example.com (won't deliver; used only to find the contact)."),
17409
17427
  testPhone: import_zod45.z.string().optional().describe("Include a phone on the submission (enables SMS-path checks). Must be a test-safe number you control \u2014 real SMS may send + bill."),
17410
- submitBody: import_zod45.z.record(import_zod45.z.unknown()).optional().describe("Full JSON body to POST (advanced; for non-bridge endpoints). The sentinel email is injected if absent. Default body is the lead-bridge contract {first_name,last_name,email,phone,custom,_hp}."),
17428
+ submitBody: import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown()).optional().describe("Full JSON body to POST (advanced; for non-bridge endpoints). The sentinel email is injected if absent. Default body is the lead-bridge contract {first_name,last_name,email,phone,custom,_hp}."),
17411
17429
  expectCustom: import_zod45.z.array(customFieldInput).optional().describe("Custom fields the form sends \u2014 each is asserted to PERSIST with the sent value (field-value fidelity, Carlos #3)."),
17412
17430
  expectStandard: import_zod45.z.array(import_zod45.z.string()).optional().describe("Standard contact keys to assert saved (e.g. ['phone','first_name']). email is always asserted."),
17413
17431
  triggerTag: import_zod45.z.string().optional().describe("The tag the form/bridge should add (the speed-to-lead trigger). Asserted to land on the contact."),
@@ -17665,11 +17683,12 @@ var init_funnel_qa = __esm({
17665
17683
 
17666
17684
  // src/tools/pipeline-builder.ts
17667
17685
  function registerPipelineBuilderTools(server2, builderClient) {
17686
+ if (!builderClient) return;
17668
17687
  const client = builderClient;
17669
- if (!client) return;
17670
17688
  async function pipelineRequest(method, path39, body) {
17671
17689
  const headers = await client.buildHeaders();
17672
17690
  const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path39}`;
17691
+ assertWriteAllowed(method, url, { defaultLocationId: client.locationId });
17673
17692
  const options = { method, headers };
17674
17693
  if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
17675
17694
  options.body = JSON.stringify(body);
@@ -17852,6 +17871,7 @@ var init_pipeline_builder = __esm({
17852
17871
  "src/tools/pipeline-builder.ts"() {
17853
17872
  "use strict";
17854
17873
  import_zod46 = require("zod");
17874
+ init_account_rules();
17855
17875
  }
17856
17876
  });
17857
17877
 
@@ -18580,19 +18600,20 @@ function deviceFingerprint(opts) {
18580
18600
  function base64urlDecode(s) {
18581
18601
  const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
18582
18602
  const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
18583
- return new Uint8Array(Buffer.from(padded, "base64"));
18603
+ return new Uint8Array(new Uint8Array(Buffer.from(padded, "base64")).buffer);
18584
18604
  }
18585
18605
  async function getPublicKey() {
18586
18606
  if (cachedPublicKey) return cachedPublicKey;
18587
18607
  const raw = Buffer.from(ATTESTATION_PUBLIC_KEY_B64, "base64");
18588
- cachedPublicKey = await import_node_crypto3.webcrypto.subtle.importKey(
18608
+ const key = await import_node_crypto3.webcrypto.subtle.importKey(
18589
18609
  "raw",
18590
18610
  raw,
18591
18611
  { name: "Ed25519" },
18592
18612
  false,
18593
18613
  ["verify"]
18594
18614
  );
18595
- return cachedPublicKey;
18615
+ cachedPublicKey = key;
18616
+ return key;
18596
18617
  }
18597
18618
  function parseAttestationPayload(token2) {
18598
18619
  try {
@@ -20007,7 +20028,8 @@ function registerValidatorTools(server2, client, builderClient) {
20007
20028
  const workflow = await builderClient.getWorkflow(workflowId);
20008
20029
  if (!workflow) return errorResponse(new Error(`Workflow ${workflowId} not found`));
20009
20030
  const refs = [];
20010
- const socialPlatforms = await connectedSocialPlatforms(client, client.defaultLocationId);
20031
+ const locationId2 = client.resolveLocationId();
20032
+ const socialPlatforms = await connectedSocialPlatforms(client, locationId2);
20011
20033
  const shape = [
20012
20034
  ...checkActionShapes(workflow),
20013
20035
  ...checkSaveBlockers(workflow),
@@ -20020,7 +20042,7 @@ function registerValidatorTools(server2, client, builderClient) {
20020
20042
  return jsonResponse({ workflowId, workflowName: workflow.name, ...summarizeFindings(shape), references_scanned: 0, actions_checked: actionsChecked, findings: shape });
20021
20043
  const needWorkflows = refs.some((r) => r.kind === "workflow");
20022
20044
  const catalog2 = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
20023
- const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog2.ids, complete: catalog2.complete });
20045
+ const lookups = await fetchAndBuildLookups(client, builderClient, locationId2, { ids: catalog2.ids, complete: catalog2.complete });
20024
20046
  const findings = [...shape, ...checkRefs(refs, workflowId, lookups)];
20025
20047
  const report2 = {
20026
20048
  workflowId,
@@ -20042,7 +20064,7 @@ function registerValidatorTools(server2, client, builderClient) {
20042
20064
  {},
20043
20065
  async () => {
20044
20066
  try {
20045
- const locationId2 = client.defaultLocationId;
20067
+ const locationId2 = client.resolveLocationId();
20046
20068
  const catalog2 = await fullWorkflowCatalog(builderClient);
20047
20069
  if (catalog2.rows.length === 0)
20048
20070
  return jsonResponse({
@@ -24836,7 +24858,7 @@ var init_plan = __esm({
24836
24858
  workflows: import_zod51.z.array(workflowSchema).optional(),
24837
24859
  handoffs: import_zod51.z.array(handoffSchema).optional(),
24838
24860
  buildOrder: import_zod51.z.array(import_zod51.z.string()).optional(),
24839
- idMap: import_zod51.z.record(import_zod51.z.string()).optional()
24861
+ idMap: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.string()).optional()
24840
24862
  }).strict();
24841
24863
  PLAN_ERROR_CODES = {
24842
24864
  /** A workflow named like /nurture/i whose waits add up to < 30 days. */
@@ -26374,15 +26396,29 @@ function newRunId(now = /* @__PURE__ */ new Date(), rand = defaultRand) {
26374
26396
  function defaultRand() {
26375
26397
  return Math.floor(Math.random() * 65536).toString(16).padStart(4, "0");
26376
26398
  }
26399
+ function legacyFunnelParentage(plan) {
26400
+ const funnels = plan.funnels;
26401
+ if (!Array.isArray(funnels)) return [];
26402
+ const pairs = [];
26403
+ for (const funnel of funnels) {
26404
+ if (typeof funnel !== "object" || funnel === null) continue;
26405
+ const { ref, pages } = funnel;
26406
+ if (typeof ref !== "string" || !Array.isArray(pages)) continue;
26407
+ for (const page of pages) {
26408
+ if (typeof page !== "object" || page === null) continue;
26409
+ const pageRef = page.ref;
26410
+ if (typeof pageRef === "string") pairs.push([pageRef, ref]);
26411
+ }
26412
+ }
26413
+ return pairs;
26414
+ }
26377
26415
  function parentRefIndex(plan) {
26378
26416
  const parents = /* @__PURE__ */ new Map();
26379
26417
  if (!plan) return parents;
26380
26418
  for (const p of plan.pipelines ?? []) {
26381
26419
  for (const st of p.stages ?? []) parents.set(st.ref, p.ref);
26382
26420
  }
26383
- for (const fn of plan.funnels ?? []) {
26384
- for (const pg of fn.pages ?? []) parents.set(pg.ref, fn.ref);
26385
- }
26421
+ for (const [pageRef, funnelRef] of legacyFunnelParentage(plan)) parents.set(pageRef, funnelRef);
26386
26422
  return parents;
26387
26423
  }
26388
26424
  function buildRunRecord(input) {
@@ -26955,8 +26991,13 @@ async function clientPreflight(registry2, locationId2) {
26955
26991
  }
26956
26992
  } catch {
26957
26993
  }
26958
- lines.push({ key: "authentication", ok: authenticated, text: authenticated ? "The Anthropic key authenticates." : key ? "The Anthropic key did not authenticate. Paste a working key in Your agency and press Check and save this key." : "Paste your Anthropic key in Your agency and press Check and save this key." });
26959
- return { ok: lines.every((l) => l.ok), lines };
26994
+ lines.push({
26995
+ key: "authentication",
26996
+ ok: authenticated,
26997
+ text: authenticated ? "Your Anthropic key authenticates, so powered runs will bill it." : key ? "The saved Anthropic key did not authenticate, so powered runs will fall back to your Claude sign-in on this computer. Replace it in Your agency, or remove it." : "No Anthropic key saved. Powered runs use your Claude sign-in on this computer and cost you nothing extra."
26998
+ });
26999
+ const blocking = lines.filter((l) => l.key !== "authentication");
27000
+ return { ok: blocking.every((l) => l.ok), lines };
26960
27001
  }
26961
27002
  async function agencyConnectionReady(registry2, companyId) {
26962
27003
  const key = registry2.getAgencyKey();
@@ -28540,7 +28581,7 @@ var require_package = __commonJS({
28540
28581
  "package.json"(exports2, module2) {
28541
28582
  module2.exports = {
28542
28583
  name: "@elitedcs/ghl-mcp",
28543
- version: "3.83.1",
28584
+ version: "3.83.3",
28544
28585
  mcpName: "io.github.drjerryrelth/ghl-command",
28545
28586
  description: "GoHighLevel MCP Server for Claude. 250 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.",
28546
28587
  main: "dist/index.js",
@@ -28555,11 +28596,6 @@ var require_package = __commonJS({
28555
28596
  "src/command-os/design/rules/anti-default.json",
28556
28597
  "src/command-os/design/rules/web.json",
28557
28598
  "src/command-os/design/rules/anti-default-contract.mjs",
28558
- "src/command-os/design/rules/check-static.mjs",
28559
- "src/command-os/design/rules/fixtures/claude-default.html",
28560
- "src/command-os/design/rules/fixtures/house-clean.html",
28561
- "src/command-os/design/rules/fixtures/generator-run-1.html",
28562
- "src/command-os/design/rules/fixtures/external-css.html",
28563
28599
  "dist/index.js",
28564
28600
  "dist/capture-helper.js",
28565
28601
  "templates/action-schemas.json",
@@ -28577,6 +28613,7 @@ var require_package = __commonJS({
28577
28613
  ],
28578
28614
  scripts: {
28579
28615
  build: "node src/command-os/assessment/build-app.mjs && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
28616
+ typecheck: "tsc --noEmit",
28580
28617
  setup: "node setup-wizard.mjs",
28581
28618
  catalogue: "node scripts/export-command-os-catalogue.mjs",
28582
28619
  "manifest:write": "node scripts/write-package-manifest.mjs",
@@ -29001,7 +29038,8 @@ function writeClientEngagement(locationId2, patch, file2 = clientEngagementsPath
29001
29038
  throw new Error("Something else saved this record while you were editing. Nothing was changed \u2014 reload and enter it again.");
29002
29039
  }
29003
29040
  function floorFor(e, profile) {
29004
- const offer = e.offer && profile.offers?.find((o) => o.name.trim().toLowerCase() === e.offer.trim().toLowerCase());
29041
+ const wanted = e.offer?.trim().toLowerCase();
29042
+ const offer = wanted ? profile.offers?.find((o) => o.name.trim().toLowerCase() === wanted) : void 0;
29005
29043
  if (offer?.priceFloor !== void 0) return offer.priceFloor;
29006
29044
  if (e.cadence === "monthly") return profile.pricing?.minMonthly;
29007
29045
  if (e.cadence === "one-time") return profile.pricing?.minSetup;
@@ -30024,13 +30062,14 @@ function pickLatest(notes) {
30024
30062
  }
30025
30063
  return null;
30026
30064
  }
30027
- var PIPELINE_NAME, NOTE_TITLE, MACHINE_MARKER, MARKER_LINE, NOTE_LIMIT;
30065
+ var PIPELINE_NAME, NOTE_TITLE, MACHINE_MARKER, STAGES, MARKER_LINE, NOTE_LIMIT;
30028
30066
  var init_store2 = __esm({
30029
30067
  "src/command-os/assessment/store.ts"() {
30030
30068
  "use strict";
30031
30069
  PIPELINE_NAME = "Assessments";
30032
30070
  NOTE_TITLE = "GHL COMMAND ASSESSMENT";
30033
30071
  MACHINE_MARKER = "--- machine block, do not edit below this line ---";
30072
+ STAGES = ["Gathering", "Findings", "Proposal sent", "Won", "Lost"];
30034
30073
  MARKER_LINE = /^--- machine block, do not edit below this line ---(?: \((\d+)\/(\d+)(?: s=([a-z0-9]{6}))?\))?[ \t]*$/gm;
30035
30074
  NOTE_LIMIT = 2e4;
30036
30075
  }
@@ -30044,7 +30083,8 @@ async function loadAssessmentRecord(reader, contactId) {
30044
30083
  }
30045
30084
  async function findAssessmentPipeline(reader, locationId2) {
30046
30085
  const raw = await reader.get("/opportunities/pipelines", { params: { locationId: locationId2 } });
30047
- return (raw?.pipelines ?? []).find((p) => String(p.name).trim().toLowerCase() === PIPELINE_NAME.toLowerCase());
30086
+ const pipelines = Array.isArray(raw?.pipelines) ? raw.pipelines : [];
30087
+ return pipelines.find((p) => String(p.name).trim().toLowerCase() === PIPELINE_NAME.toLowerCase());
30048
30088
  }
30049
30089
  async function listAssessmentRecords(reader, locationId2) {
30050
30090
  const pipeline = await findAssessmentPipeline(reader, locationId2);
@@ -33394,7 +33434,7 @@ function registerAssessmentTools(server2, client) {
33394
33434
  quote: import_zod52.z.string().describe("The owner's own words, VERBATIM from the transcript. Do not tidy it up \u2014 if it is not in the transcript the answer is thrown out."),
33395
33435
  reasoning: import_zod52.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
33396
33436
  })).describe("One entry per question you could answer. Leave out anything the conversation did not cover."),
33397
- existing: import_zod52.z.record(import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
33437
+ existing: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
33398
33438
  },
33399
33439
  async ({ transcript, proposed, existing }) => {
33400
33440
  const gated = gateExtraction(transcript, proposed, QUESTION_UNITS);
@@ -33517,9 +33557,9 @@ var init_assessment = __esm({
33517
33557
  StateSchema = import_zod52.z.object({
33518
33558
  biz: import_zod52.z.string().min(1).describe("The prospect's business name."),
33519
33559
  trade: import_zod52.z.string().optional().describe("Their trade. Decides which benchmarks may be applied."),
33520
- ans: import_zod52.z.record(import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).describe("questionId -> answer, exactly as typed."),
33521
- skip: import_zod52.z.record(import_zod52.z.boolean()).optional().describe("questionId -> true where they did not know."),
33522
- asm: import_zod52.z.record(import_zod52.z.number()).optional().describe("assumption id -> rate as a decimal."),
33560
+ ans: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).describe("questionId -> answer, exactly as typed."),
33561
+ skip: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.boolean()).optional().describe("questionId -> true where they did not know."),
33562
+ asm: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.number()).optional().describe("assumption id -> rate as a decimal."),
33523
33563
  client: import_zod52.z.object({ name: import_zod52.z.string().max(120), role: import_zod52.z.string().max(80), email: import_zod52.z.string().max(200), phone: import_zod52.z.string().max(40), website: import_zod52.z.string().max(300), city: import_zod52.z.string().max(120) }).partial().optional().describe("The person and place this is for, as typed on The client step. Rides with the answers so a load brings it back.")
33524
33564
  });
33525
33565
  }
@@ -34096,7 +34136,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
34096
34136
  const obligations = (context.manualObligations ?? []).filter((o) => !deps.some((d) => d.ref === o.ref) && !(outcome.manualChecks ?? []).some((c) => c.ref === o.ref && c.type === o.type && c.ok && c.evidence.trim()));
34097
34137
  for (const h of plan?.plan?.handoffs ?? []) if (!obligations.some((o) => o.ref === h.ref) && !(outcome.manualChecks ?? []).some((c) => c.ref === h.ref && c.ok && c.evidence.trim())) obligations.push({ ref: h.ref, type: "handoff", name: h.title || h.name || h.ref, reason: h.instruction || "Finish this manual action." });
34098
34138
  outcome.ok = verifyBuildPassed(outcome, readback, plan?.plan);
34099
- verifyRaw = { summary: outcome.summary, issues: outcome.issues, verification: { deps, checks, readback, obligations, buildOk: outcome.ok, liveProofApplicable: Boolean(plan?.plan?.funnels?.length), liveProof: outcome.liveProof ?? { ok: false, evidence: outcome.error || "No test-lead result was returned." } } };
34139
+ verifyRaw = { summary: outcome.summary, issues: outcome.issues, verification: { deps, checks, readback, obligations, buildOk: outcome.ok, liveProofApplicable: Boolean(plan?.plan && typeof plan.plan === "object" && "funnels" in plan.plan && Array.isArray(plan.plan.funnels) && plan.plan.funnels.length), liveProof: outcome.liveProof ?? { ok: false, evidence: outcome.error || "No test-lead result was returned." } } };
34100
34140
  if (!plan?.plan) {
34101
34141
  outcome.ok = false;
34102
34142
  verifyRaw.issues = [...verifyRaw.issues ?? [], "Run Build before verifying."];
@@ -35190,7 +35230,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
35190
35230
  el.appendChild(box)}
35191
35231
  // One short line here; the button's confirmation carries the full wording. At 135
35192
35232
  // characters this sentence made every stage box 677 px tall (auditor, 2026-09-07).
35193
- if(i===4){const disclosure=document.createElement("small");disclosure.textContent="Submits one test lead, then deletes it.";el.appendChild(disclosure)}
35233
+ if(i===4){const disclosure=document.createElement("small");disclosure.textContent="Reads every planned item back. May submit and delete one test lead.";el.appendChild(disclosure)}
35194
35234
  if(gate)el.title=gate;
35195
35235
  if(stale&&!live){const reason=document.createElement("small");reason.textContent=stale.reason;el.appendChild(reason)}
35196
35236
  const oc=(state.outcomes||{})[c.locationId+":"+i];
@@ -36987,8 +37027,9 @@ async function runDashboard(argv) {
36987
37027
  state = setAccountType(state, outcome.locationId, "client");
36988
37028
  if (args.industry) state = setAccountIndustry(state, outcome.locationId, args.industry);
36989
37029
  if (!outcome.registered) {
36990
- state = setPending(state, outcome.locationId, args.name, args.industry || void 0);
36991
- if (outcome.saveFailed) state.pending[outcome.locationId].saveFailed = true;
37030
+ const pending = setPending(state, outcome.locationId, args.name, args.industry || void 0);
37031
+ if (outcome.saveFailed) pending.pending[outcome.locationId].saveFailed = true;
37032
+ state = pending;
36992
37033
  }
36993
37034
  writeCockpitState(state);
36994
37035
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -37996,7 +38037,7 @@ function registerBulkOperationTools(server2, client) {
37996
38037
  "Update the same field(s) on multiple contacts at once. Rate-limited. Example: set a custom field value, change source, update address for a batch of contacts.",
37997
38038
  {
37998
38039
  contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
37999
- fields: import_zod54.z.record(import_zod54.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
38040
+ fields: import_zod54.z.record(import_zod54.z.string(), import_zod54.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
38000
38041
  },
38001
38042
  async ({ contactIds, fields }) => {
38002
38043
  if (Object.keys(fields).length === 0) {
@@ -39054,10 +39095,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
39054
39095
  workflowId: void 0
39055
39096
  // Will be set by the new workflow
39056
39097
  };
39057
- if (cloned.data && typeof cloned.data === "object" && "targetActionId" in cloned.data) {
39098
+ const clonedData = cloned.data;
39099
+ if (clonedData && typeof clonedData === "object" && "targetActionId" in clonedData) {
39100
+ const targetActionId = clonedData.targetActionId;
39058
39101
  cloned.data = {
39059
- ...cloned.data,
39060
- targetActionId: remap(cloned.data.targetActionId)
39102
+ ...clonedData,
39103
+ targetActionId: typeof targetActionId === "string" ? remap(targetActionId) : targetActionId
39061
39104
  };
39062
39105
  }
39063
39106
  return cloned;
@@ -39170,11 +39213,12 @@ function buildContactSmartListBody(args) {
39170
39213
  };
39171
39214
  }
39172
39215
  function registerSmartListTools(server2, builderClient) {
39216
+ if (!builderClient) return;
39173
39217
  const client = builderClient;
39174
- if (!client) return;
39175
39218
  async function smartListRequest(method, path39, body) {
39176
39219
  const headers = await client.buildHeaders();
39177
39220
  const url = `${SMARTLIST_BASE}${path39}`;
39221
+ assertWriteAllowed(method, url, { defaultLocationId: client.locationId });
39178
39222
  const options = { method, headers };
39179
39223
  if (body && (method === "POST" || method === "PUT")) {
39180
39224
  options.body = JSON.stringify(body);
@@ -39232,7 +39276,7 @@ ${await res.text()}`);
39232
39276
  {
39233
39277
  name: import_zod57.z.string().describe("Display name for the smart list."),
39234
39278
  tagFilters: import_zod57.z.array(import_zod57.z.array(import_zod57.z.string())).optional().describe('Tag conditions. Each inner array is one filter requiring ALL those tags, e.g. [["escalated"]] for "has the escalated tag".'),
39235
- rawFilters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.unknown())).optional().describe("Advanced: raw filter leaves {field, operator, value, options}. Combined with tagFilters into one AND group."),
39279
+ rawFilters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Advanced: raw filter leaves {field, operator, value, options}. Combined with tagFilters into one AND group."),
39236
39280
  locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39237
39281
  },
39238
39282
  async ({ name, tagFilters, rawFilters, locationId: locationId2 }) => {
@@ -39244,6 +39288,7 @@ ${await res.text()}`);
39244
39288
  ];
39245
39289
  const body = buildContactSmartListBody({ listName: name, locationId: loc, filters });
39246
39290
  const headers = await client.buildHeaders();
39291
+ assertWriteAllowed("POST", CONTACT_SMARTLIST_BASE, { defaultLocationId: client.locationId });
39247
39292
  const res = await fetch(CONTACT_SMARTLIST_BASE, { method: "POST", headers, body: JSON.stringify(body) });
39248
39293
  if (!res.ok) throw new Error(`Smart Lists API Error ${res.status}: POST /contacts/smartlist
39249
39294
  ${await res.text()}`);
@@ -39281,6 +39326,7 @@ ${await res.text()}`);
39281
39326
  async ({ listId }) => {
39282
39327
  try {
39283
39328
  const headers = await client.buildHeaders();
39329
+ assertWriteAllowed("POST", CONTACT_SMARTLIST_DELETE, { defaultLocationId: client.locationId });
39284
39330
  const res = await fetch(CONTACT_SMARTLIST_DELETE, {
39285
39331
  method: "POST",
39286
39332
  headers,
@@ -39317,8 +39363,8 @@ ${await res.text()}`);
39317
39363
  {
39318
39364
  name: import_zod57.z.string().describe("Display name for the smart list."),
39319
39365
  objectKey: import_zod57.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
39320
- filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
39321
- columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
39366
+ filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
39367
+ columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
39322
39368
  pipelineIds: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
39323
39369
  defaultInPipelines: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
39324
39370
  locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
@@ -39344,8 +39390,8 @@ ${await res.text()}`);
39344
39390
  {
39345
39391
  listId: import_zod57.z.string().describe("The smart list ID to update."),
39346
39392
  name: import_zod57.z.string().optional().describe("New display name."),
39347
- filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
39348
- columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.unknown())).optional().describe("Replace the column array entirely."),
39393
+ filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
39394
+ columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Replace the column array entirely."),
39349
39395
  pipelineIds: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
39350
39396
  defaultInPipelines: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
39351
39397
  locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
@@ -39394,6 +39440,7 @@ var init_smart_lists = __esm({
39394
39440
  "use strict";
39395
39441
  import_zod57 = require("zod");
39396
39442
  init_tool_helpers();
39443
+ init_account_rules();
39397
39444
  SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
39398
39445
  OBJECT_KEYS = ["contacts", "opportunity"];
39399
39446
  CONTACT_SMARTLIST_BASE = "https://services.leadconnectorhq.com/contacts/smartlist";
@@ -39412,9 +39459,10 @@ var init_smart_lists = __esm({
39412
39459
 
39413
39460
  // src/tools/reputation.ts
39414
39461
  function registerReputationTools(server2, builderClient) {
39462
+ if (!builderClient) return;
39415
39463
  const client = builderClient;
39416
- if (!client) return;
39417
39464
  async function reputationRequest(method, path39) {
39465
+ assertWriteAllowed(method, `${REPUTATION_BASE}${path39}`, { defaultLocationId: client.locationId });
39418
39466
  const headers = await client.buildHeaders();
39419
39467
  const response = await fetch(`${REPUTATION_BASE}${path39}`, { method, headers });
39420
39468
  if (!response.ok) {
@@ -39493,6 +39541,7 @@ var init_reputation = __esm({
39493
39541
  "use strict";
39494
39542
  import_zod58 = require("zod");
39495
39543
  init_tool_helpers();
39544
+ init_account_rules();
39496
39545
  REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
39497
39546
  }
39498
39547
  });
@@ -39527,6 +39576,7 @@ function registerEmailCampaignTools(server2, builderClient) {
39527
39576
  if (enableResendToUnopened !== void 0) body.enableResendToUnopened = enableResendToUnopened;
39528
39577
  if (hasUtmTracking !== void 0) body.hasUtmTracking = hasUtmTracking;
39529
39578
  const headers = await client.buildHeaders();
39579
+ assertWriteAllowed("POST", `${SVC_BASE}/emails/schedule`, { defaultLocationId: client.locationId });
39530
39580
  const response = await fetch(`${SVC_BASE}/emails/schedule`, {
39531
39581
  method: "POST",
39532
39582
  headers,
@@ -39552,15 +39602,17 @@ var init_email_campaigns = __esm({
39552
39602
  "use strict";
39553
39603
  import_zod59 = require("zod");
39554
39604
  init_tool_helpers();
39605
+ init_account_rules();
39555
39606
  SVC_BASE = "https://services.leadconnectorhq.com";
39556
39607
  }
39557
39608
  });
39558
39609
 
39559
39610
  // src/tools/memberships.ts
39560
39611
  function registerMembershipTools(server2, builderClient) {
39612
+ if (!builderClient) return;
39561
39613
  const client = builderClient;
39562
- if (!client) return;
39563
39614
  async function membershipRequest(path39, method = "GET", body) {
39615
+ assertWriteAllowed(method, `${MEMBERSHIP_BASE2}${path39}`, { defaultLocationId: client.locationId });
39564
39616
  const headers = await client.buildHeaders();
39565
39617
  const response = await fetch(`${MEMBERSHIP_BASE2}${path39}`, {
39566
39618
  method,
@@ -39748,6 +39800,7 @@ var init_memberships = __esm({
39748
39800
  "use strict";
39749
39801
  import_zod60 = require("zod");
39750
39802
  init_tool_helpers();
39803
+ init_account_rules();
39751
39804
  MEMBERSHIP_BASE2 = "https://backend.leadconnectorhq.com/membership";
39752
39805
  }
39753
39806
  });
@@ -39875,7 +39928,7 @@ function registerTemplateDeployerTools(server2, client) {
39875
39928
  "Deploy a template to set up a GHL sub-account. Creates tags, custom fields, pipelines with stages, calendars, workflows, and forms based on the template and the user's questionnaire answers. This is the main setup automation tool.",
39876
39929
  {
39877
39930
  templateFile: import_zod61.z.string().describe("Path to the template JSON file."),
39878
- answers: import_zod61.z.record(import_zod61.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
39931
+ answers: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
39879
39932
  locationId: import_zod61.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
39880
39933
  dryRun: import_zod61.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false."),
39881
39934
  overwriteBusinessInfo: import_zod61.z.boolean().optional().describe(
@@ -40096,6 +40149,7 @@ Pipelines: ${template.pipelines.length} to create`);
40096
40149
  } else {
40097
40150
  try {
40098
40151
  const headers = await builderClient.buildHeaders();
40152
+ assertWriteAllowed("POST", "https://backend.leadconnectorhq.com/opportunities/pipelines", { defaultLocationId: locId });
40099
40153
  const res = await fetch("https://backend.leadconnectorhq.com/opportunities/pipelines", {
40100
40154
  method: "POST",
40101
40155
  headers,
@@ -40270,6 +40324,7 @@ var init_template_deployer = __esm({
40270
40324
  init_tool_helpers();
40271
40325
  fs28 = __toESM(require("fs"));
40272
40326
  path27 = __toESM(require("path"));
40327
+ init_account_rules();
40273
40328
  TemplateSchema = import_zod61.z.object({
40274
40329
  templateName: import_zod61.z.string(),
40275
40330
  templateVersion: import_zod61.z.string().optional(),
@@ -40281,7 +40336,7 @@ var init_template_deployer = __esm({
40281
40336
  required: import_zod61.z.boolean().optional(),
40282
40337
  placeholder: import_zod61.z.string().optional()
40283
40338
  })).optional().default([]),
40284
- location: import_zod61.z.record(import_zod61.z.unknown()).optional(),
40339
+ location: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown()).optional(),
40285
40340
  tags: import_zod61.z.array(import_zod61.z.string()).optional(),
40286
40341
  customFields: import_zod61.z.array(import_zod61.z.object({
40287
40342
  name: import_zod61.z.string(),
@@ -40294,7 +40349,7 @@ var init_template_deployer = __esm({
40294
40349
  workflows: import_zod61.z.array(import_zod61.z.object({
40295
40350
  name: import_zod61.z.string(),
40296
40351
  condition: import_zod61.z.string().optional(),
40297
- actions: import_zod61.z.array(import_zod61.z.record(import_zod61.z.unknown())).optional().default([])
40352
+ actions: import_zod61.z.array(import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown())).optional().default([])
40298
40353
  })).optional(),
40299
40354
  calendars: import_zod61.z.array(import_zod61.z.object({
40300
40355
  name: import_zod61.z.string(),
@@ -40334,13 +40389,17 @@ var init_version_check = __esm({
40334
40389
  });
40335
40390
 
40336
40391
  // src/tools/diagnostics.ts
40392
+ function anthropicKeyCheckStatus(verdict) {
40393
+ const severity = ANTHROPIC_KEY_STATUS[verdict];
40394
+ return severity === "ok" ? "pass" : severity;
40395
+ }
40337
40396
  async function collectInstallChecks(installedVersion, client, builderClient, registry2, getGatingReport, plan = "mcp", probeAnthropic = probeAnthropicKey) {
40338
40397
  const checks = [];
40339
40398
  const anthropicPromise = plan === "command-os" ? (async () => {
40340
40399
  const r = await probeAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
40341
40400
  return {
40342
40401
  name: "Anthropic key (Command OS demo builder)",
40343
- status: ANTHROPIC_KEY_STATUS[r.verdict],
40402
+ status: anthropicKeyCheckStatus(r.verdict),
40344
40403
  detail: r.raw ? `${r.detail} Anthropic said: "${r.raw}"` : r.detail
40345
40404
  };
40346
40405
  })() : null;
@@ -46256,7 +46315,7 @@ function registerAgencyBuildTools(server2, knownTools) {
46256
46315
  why: import_zod72.z.string().min(3).describe("Plain English. This is what the operator reads before approving."),
46257
46316
  targets: import_zod72.z.array(targetSchema).default([]),
46258
46317
  creates: import_zod72.z.array(import_zod72.z.object({ ref: import_zod72.z.string(), kind: import_zod72.z.string() })).optional().describe("What this step brings into existence, for later steps to point at."),
46259
- args: import_zod72.z.record(import_zod72.z.unknown()).optional().describe("The arguments, kept verbatim \u2014 the approval covers these, so changing one voids it.")
46318
+ args: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.unknown()).optional().describe("The arguments, kept verbatim \u2014 the approval covers these, so changing one voids it.")
46260
46319
  })).min(1)
46261
46320
  },
46262
46321
  async ({ goal, findings, steps }) => {
@@ -46326,7 +46385,7 @@ function registerAgencyBuildTools(server2, knownTools) {
46326
46385
  undo: import_zod72.z.string().optional().describe("Required on 'applied'. How to reverse this exact write, specifically enough to act on months later."),
46327
46386
  because: import_zod72.z.string().optional().describe("Required on 'failed'. What the account actually said."),
46328
46387
  result: import_zod72.z.unknown().optional().describe("What came back, kept so a stall can be diagnosed without re-running anything."),
46329
- resolved: import_zod72.z.record(import_zod72.z.string()).optional().describe("The real ids this step created, keyed by the ref later steps use."),
46388
+ resolved: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.string()).optional().describe("The real ids this step created, keyed by the ref later steps use."),
46330
46389
  retry: import_zod72.z.boolean().optional().describe("This is a second run of a step that failed. Both attempts are kept, and the earlier failure stops blocking what came after it.")
46331
46390
  },
46332
46391
  async ({ buildId, stepId, state, undo, because, result, resolved: resolved2, retry }) => {
@@ -47191,7 +47250,7 @@ function registerAuditReportTools(server2) {
47191
47250
  "build_audit_report",
47192
47251
  "Turn the output of `audit_workflows` into a client-ready assessment: plain-English findings with money attached where it can honestly be attached, ranked worst-first, plus an explicit list of what could NOT be checked. Call `audit_workflows` first and pass its result here \u2014 this does not re-read the account. Give `leadsPerMonth`, `closeRate` and `monthlyValuePerClient` if you know them and the findings get costed in dollars; leave them out and it says which input is missing instead of guessing. `monthlyValuePerClient` defaults to your saved minimum monthly fee from your agency profile. Use this to produce the document you send or present, not to diagnose \u2014 the diagnosing already happened.",
47193
47252
  {
47194
- audit: import_zod74.z.record(import_zod74.z.unknown()).describe("The full JSON result returned by audit_workflows."),
47253
+ audit: import_zod74.z.record(import_zod74.z.string(), import_zod74.z.unknown()).describe("The full JSON result returned by audit_workflows."),
47195
47254
  audience: import_zod74.z.enum(["internal", "prospect"]).optional().describe("Who reads it. 'internal' (default) is a report about an account you already run, and may use your own minimum monthly as what a client is worth. 'prospect' is a document about somebody else's business: your numbers are NEVER used, so findings stay uncosted unless you supply THEIR lead volume, close rate and client value."),
47196
47255
  accountName: import_zod74.z.string().describe("The client's name as they should see it on the document."),
47197
47256
  locationId: import_zod74.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
@@ -47256,7 +47315,7 @@ function registerAuditReportTools(server2) {
47256
47315
  "build_site_audit_report",
47257
47316
  "Turn a website read-through into the assessment document you put in front of a PROSPECT \u2014 someone whose GoHighLevel account you do not have. Pass the verified observations from a site crawl and it returns plain-English findings, ranked, each one carrying the quote and the page it came from. Use it before a sales call, or alongside a receptionist demo, so the same read of their site fills both. It never invents money: a website cannot tell you what a lead is worth to that business, so findings come back uncosted unless you supply THEIR figures, and the document says why. It also never repeats a claim the crawl could not stand up \u2014 anything refused is returned separately, so you can see what was thrown out rather than wonder what was missed.",
47258
47317
  {
47259
- observations: import_zod74.z.record(import_zod74.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
47318
+ observations: import_zod74.z.record(import_zod74.z.string(), import_zod74.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
47260
47319
  businessName: import_zod74.z.string().describe("The business's name as it should appear on the document."),
47261
47320
  leadsPerMonth: import_zod74.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
47262
47321
  closeRate: import_zod74.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
@@ -48138,7 +48197,7 @@ var init_brief2 = __esm({
48138
48197
  preset: presetSchema,
48139
48198
  briefSource: briefSourceSchema,
48140
48199
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
48141
- extended: import_zod79.z.record(import_zod79.z.unknown()).optional(),
48200
+ extended: import_zod79.z.record(import_zod79.z.string(), import_zod79.z.unknown()).optional(),
48142
48201
  business: import_zod79.z.object({
48143
48202
  name: import_zod79.z.string(),
48144
48203
  type: import_zod79.z.string().optional(),
@@ -49913,7 +49972,7 @@ async function executeBackbone(plan, deps, opts = {}) {
49913
49972
  pendStaff(u, `Staff member "${name}" (${u.email}) was not created: no agency key is registered (creating users is an agency-level write). Run register_agency_key with an agency-level Private Integration key that has users.write, then re-run \u2014 or add them in Settings \u2192 Team and re-run to bind them by email.`);
49914
49973
  continue;
49915
49974
  }
49916
- let outcome;
49975
+ let outcome = void 0;
49917
49976
  let recoveredUser;
49918
49977
  try {
49919
49978
  outcome = await deps.createUser({ firstName: u.firstName, lastName: u.lastName, email: u.email, role: u.role, phone: u.phone });
@@ -50927,6 +50986,7 @@ function pickCalendarPins(workflow) {
50927
50986
  }
50928
50987
  function makeFirebaseRequest(builderClient) {
50929
50988
  return async (url, method, body) => {
50989
+ assertWriteAllowed(method, url, { defaultLocationId: builderClient.locationId });
50930
50990
  const headers = await builderClient.buildHeaders();
50931
50991
  const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : void 0 });
50932
50992
  if (!response.ok) {
@@ -51353,14 +51413,14 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51353
51413
  "validate_brief",
51354
51414
  "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.",
51355
51415
  {
51356
- brief: import_zod80.z.record(import_zod80.z.unknown()).describe("The Brief object to validate.")
51416
+ brief: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).describe("The Brief object to validate.")
51357
51417
  },
51358
51418
  async ({ brief }) => validateBrief(brief)
51359
51419
  );
51360
51420
  const buildPlanSchema2 = {
51361
- plan: import_zod80.z.record(import_zod80.z.unknown()).optional().describe("The approved \xA75 Build Plan object. Omit it and pass useSavedPlan:true to build from the plan already saved for this account."),
51421
+ plan: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).optional().describe("The approved \xA75 Build Plan object. Omit it and pass useSavedPlan:true to build from the plan already saved for this account."),
51362
51422
  fromPreset: import_zod80.z.boolean().optional().describe("PLAN-FROM-PRESET (PRD row 4d): compose the \xA75 plan SERVER-SIDE from the industry preset + the \xA74 brief, deterministically, in milliseconds \u2014 never author workflow actions by hand when a preset covers the brief. Pass `brief` (required) and optionally `preset` and `copy`; omit `plan`. The composed plan carries the preset's full structure (workflows, templates with complete send-ready copy token-filled from the brief, staff users, calendars with the brief's appointment lengths, pipeline stages, notification targets) and then flows through the exact same validate \u2192 dry_run/execute path as a passed plan, including being saved on execute. The dry_run response lists copySlots \u2014 the template refs whose subject/body the model MAY refine via `copy` on the execute call. If the preset cannot cover the brief the tool answers phase:\"compose\" with the reasons; only then author a plan by hand."),
51363
- brief: import_zod80.z.record(import_zod80.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
51423
+ brief: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
51364
51424
  preset: import_zod80.z.string().optional().describe("fromPreset only: which preset to compose from \u2014 a preset id (med_spa, clinic, coach, local_service, ecom, generic), an alias (dental, medspa, home_services\u2026), or an industry slug (med-spa, local-service, ecommerce\u2026). Defaults to the brief's own preset field; an unknown value falls back to the generic preset (noted in the response)."),
51365
51425
  copy: import_zod80.z.array(
51366
51426
  import_zod80.z.object({
@@ -52013,7 +52073,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
52013
52073
  {
52014
52074
  formId: import_zod80.z.string().describe("The intake form ID (from install_intake_form)."),
52015
52075
  submissionId: import_zod80.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
52016
- fieldMap: import_zod80.z.record(import_zod80.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
52076
+ fieldMap: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
52017
52077
  preset: import_zod80.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
52018
52078
  },
52019
52079
  async ({ formId, submissionId, fieldMap, preset }) => {
@@ -52100,6 +52160,7 @@ var init_intake_to_build = __esm({
52100
52160
  init_plan_from_preset();
52101
52161
  init_executor();
52102
52162
  init_execute();
52163
+ init_account_rules();
52103
52164
  customFieldItemSchema = import_zod80.z.object({
52104
52165
  id: import_zod80.z.string(),
52105
52166
  name: import_zod80.z.string(),
@@ -52145,7 +52206,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
52145
52206
  registerConnectorTools(wrap(CONNECTORS_MODULE));
52146
52207
  registerGoogleCalendarTools(wrap(GOOGLE_CALENDAR_MODULE));
52147
52208
  registerDeliveryAuditTools(wrap(DELIVERY_AUDIT_MODULE));
52148
- registerDesignStudioTools(wrap(DESIGN_STUDIO_MODULE), builderClient, client);
52209
+ registerDesignStudioTools(wrap(DESIGN_STUDIO_MODULE));
52149
52210
  registerAgencyBuildTools(wrap(AGENCY_BUILD_MODULE), () => registeredTools);
52150
52211
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
52151
52212
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -52291,9 +52352,16 @@ var init_tools = __esm({
52291
52352
  [registerAssociationTools, "associations"],
52292
52353
  [registerEstimateTools, "estimates"],
52293
52354
  [registerCouponTools, "coupons"],
52294
- [registerKnowledgeBaseTools, "knowledge-base"],
52295
- [registerConversationAiTools, "conversation-ai"],
52296
- [registerAiEmployeeDemoTools, "ai-employee-demo"],
52355
+ [(server2, client) => registerKnowledgeBaseTools(server2, client), "knowledge-base"],
52356
+ // Wrapped, not listed bare: this registrar's third parameter is a `deps`
52357
+ // object, not the TokenRegistry the loop below passes to everything in this
52358
+ // list. Today that is harmless — every key of `deps` is optional and a
52359
+ // TokenRegistry has none of them, so every read falls back to its default —
52360
+ // but it is harmless by luck, and stops being harmless the day TokenRegistry
52361
+ // grows a `sleep`, `now` or `journalDir`. The wrapper passes nothing, which
52362
+ // is what the registrar effectively receives now.
52363
+ [(server2, client) => registerConversationAiTools(server2, client), "conversation-ai"],
52364
+ [(server2, client) => registerAiEmployeeDemoTools(server2, client), "ai-employee-demo"],
52297
52365
  [registerVoiceAiTools, "voice-ai"],
52298
52366
  [registerDocumentTools, "documents"],
52299
52367
  [registerBulkOperationTools, "bulk-operations"],
@@ -52542,6 +52610,12 @@ var init_cli2 = __esm({
52542
52610
  });
52543
52611
 
52544
52612
  // src/index.ts
52613
+ var index_exports = {};
52614
+ __export(index_exports, {
52615
+ TRANSITIONAL_ATTESTATION_DEADLINE: () => TRANSITIONAL_ATTESTATION_DEADLINE,
52616
+ licenseGateFailureMessage: () => licenseGateFailureMessage
52617
+ });
52618
+ module.exports = __toCommonJS(index_exports);
52545
52619
  var dotenv2 = __toESM(require("dotenv"));
52546
52620
  var path38 = __toESM(require("path"));
52547
52621
  var fs39 = __toESM(require("fs"));
@@ -52622,6 +52696,15 @@ var os6 = __toESM(require("node:os"));
52622
52696
  function sha2562(buf) {
52623
52697
  return (0, import_node_crypto12.createHash)("sha256").update(buf).digest("hex");
52624
52698
  }
52699
+ function isOlderRelease(candidate, installed) {
52700
+ const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
52701
+ const a = parts(candidate), b = parts(installed);
52702
+ if (a.length !== 3 || b.length !== 3 || [...a, ...b].some(Number.isNaN)) return false;
52703
+ for (let i = 0; i < 3; i++) {
52704
+ if (a[i] !== b[i]) return a[i] < b[i];
52705
+ }
52706
+ return false;
52707
+ }
52625
52708
  function bundledSkillsDir(baseDir) {
52626
52709
  const candidate = path37.resolve(baseDir, "..", "skills");
52627
52710
  try {
@@ -52655,6 +52738,7 @@ function installBundledSkills(opts) {
52655
52738
  installed: [],
52656
52739
  updated: [],
52657
52740
  skippedUserModified: [],
52741
+ skippedOlderPackage: [],
52658
52742
  unchanged: [],
52659
52743
  errors: [],
52660
52744
  targetDir: opts.targetDir ?? path37.join(os6.homedir(), ".claude", "skills"),
@@ -52697,7 +52781,11 @@ function installBundledSkills(opts) {
52697
52781
  continue;
52698
52782
  }
52699
52783
  const lastWritten = marker.files[rel];
52700
- if (lastWritten && destHash === lastWritten) {
52784
+ if (lastWritten && destHash === lastWritten && isOlderRelease(opts.packageVersion, marker.packageVersion)) {
52785
+ newMarker.files[rel] = destHash;
52786
+ newMarker.packageVersion = marker.packageVersion;
52787
+ result.skippedOlderPackage.push(rel);
52788
+ } else if (lastWritten && destHash === lastWritten) {
52701
52789
  fs38.writeFileSync(dest, src);
52702
52790
  newMarker.files[rel] = srcHash;
52703
52791
  result.updated.push(rel);
@@ -52721,6 +52809,7 @@ function summarizeInstall(r) {
52721
52809
  if (r.installed.length) bits.push(`${r.installed.length} installed`);
52722
52810
  if (r.updated.length) bits.push(`${r.updated.length} updated`);
52723
52811
  if (r.skippedUserModified.length) bits.push(`${r.skippedUserModified.length} kept (your edits)`);
52812
+ if (r.skippedOlderPackage.length) bits.push(`${r.skippedOlderPackage.length} kept (newer than this version)`);
52724
52813
  if (r.errors.length) bits.push(`${r.errors.length} errors`);
52725
52814
  if (bits.length === 0) return "";
52726
52815
  return `Skills: ${bits.join(", ")} \u2192 ${r.targetDir}`;
@@ -52750,6 +52839,9 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
52750
52839
  installed: result.installed,
52751
52840
  updated: result.updated,
52752
52841
  keptYourEdits: result.skippedUserModified,
52842
+ // A member running an older install alongside a newer one would otherwise
52843
+ // have their skills rolled back without a word. BUG-2026-09-10-01.
52844
+ keptNewerThanThisVersion: result.skippedOlderPackage,
52753
52845
  unchanged: result.unchanged.length,
52754
52846
  errors: result.errors,
52755
52847
  next: result.installed.length > 0 || result.updated.length > 0 ? "Fully restart Claude (quit and reopen) so the new/updated skills load." : "Everything already current."
@@ -52909,6 +53001,12 @@ registerMetaTools(server, pkg.version);
52909
53001
  var inBootstrapMode = true;
52910
53002
  var telemetryTier = "none";
52911
53003
  var TRANSITIONAL_ATTESTATION_DEADLINE = Date.parse("2026-06-15T00:00:00.000Z");
53004
+ function licenseGateFailureMessage(lic, deadline = TRANSITIONAL_ATTESTATION_DEADLINE) {
53005
+ if (!lic.ok) return `[ghl-mcp] License check failed: ${lic.error}
53006
+ `;
53007
+ return `[ghl-mcp] Your license is valid, but our license server did not return a signed attestation, which has been required since ${new Date(deadline).toISOString().slice(0, 10)}. This is on our side, not your install. Please write support@ghlcommand.com.
53008
+ `;
53009
+ }
52912
53010
  async function renewAttestation(creds) {
52913
53011
  if (!creds.email || !creds.license_key) return "unauthorized";
52914
53012
  let lic;
@@ -52991,7 +53089,9 @@ async function resolveAccessAndRegister() {
52991
53089
  attestationStatus = "renewed-from-legacy";
52992
53090
  } else {
52993
53091
  attestationStatus = "needs-reset";
52994
- if (renewed === "cancelled" || renewed === "unauthorized") {
53092
+ if (renewed === "install_limit") {
53093
+ process.stderr.write("[ghl-mcp] Bootstrap mode. Free a device on your license, or write support@ghlcommand.com.\n");
53094
+ } else if (renewed === "cancelled" || renewed === "unauthorized") {
52995
53095
  process.stderr.write(`[ghl-mcp] Server says license is ${renewed}. Bootstrap mode.
52996
53096
  `);
52997
53097
  } else if (renewed === "renewed-unsigned") {
@@ -53028,8 +53128,7 @@ async function resolveAccessAndRegister() {
53028
53128
  } catch {
53029
53129
  }
53030
53130
  } else {
53031
- process.stderr.write(`[ghl-mcp] License check failed: ${lic.error}
53032
- `);
53131
+ process.stderr.write(licenseGateFailureMessage(lic));
53033
53132
  }
53034
53133
  } else {
53035
53134
  process.stderr.write(
@@ -53231,3 +53330,8 @@ main().catch((error) => {
53231
53330
  process.stderr.write(`[ghl-mcp] Failed to start: ${error}
53232
53331
  `);
53233
53332
  });
53333
+ // Annotate the CommonJS export names for ESM import in node:
53334
+ 0 && (module.exports = {
53335
+ TRANSITIONAL_ATTESTATION_DEADLINE,
53336
+ licenseGateFailureMessage
53337
+ });