@elitedcs/ghl-mcp 3.95.0 → 3.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.96.0: Choose which of your numbers a text goes out from, and send it exactly once
4
+
5
+ **You can now say which of an account's phone numbers a text goes out from.** Ask Claude to send a text from a specific number, for example "text Jane from +15555550100". GHL Command first checks that the number is one of that account's own lines. If it is not, nothing is sent and the answer lists the account's real numbers. If the check cannot run, nothing is sent either, and the answer says why.
6
+
7
+ **After the text goes out, GHL Command checks which number GoHighLevel reports it used.** The answer says whether it matches the number you asked for. If GoHighLevel used a different number, the answer says so plainly and tells you not to report it as sent from the number you asked for. If GoHighLevel names the sender only by the account's name instead of a number, the answer says the sender could not be confirmed.
8
+
9
+ **A text can be sent exactly once, with no automatic resend.** Normally, if the connection drops, GoHighLevel says it is busy, or GoHighLevel has an error of its own, GHL Command tries again. For a text that is a risk: the first try may already have gone out, and the person gets it twice. Ask for a single attempt and GHL Command never resends. If that one try fails, the answer says which of two things happened:
10
+ - **Not sent.** GoHighLevel refused it, or GHL Command stopped it before it left. Safe to send again.
11
+ - **May have gone out.** The connection dropped, the try timed out, or GoHighLevel had an error of its own, so there is no way to tell whether it went. Check the conversation before sending again.
12
+
13
+ **Nothing changes unless you ask for it.** Texts that use neither option work exactly as before.
14
+
3
15
  ## 3.95.0: The brain checks a message against the lead's record before it goes out
4
16
 
5
17
  **The brain can now check a message against the lead's record before it goes out.** You add one asking step in front of an SMS or email in a workflow. When a lead reaches it, the brain reads that lead's GoHighLevel record (contact, appointments, invoices and recent messages), its own notes on the lead, your business's path and your past rulings, and answers send or hold. If the brain cannot check it, a reminder still goes out and a message asking the lead for something is held for a person.
package/README.md CHANGED
@@ -904,7 +904,7 @@ Built by **[Elite DCs, LLC](https://elitedcs.com)** — a digital marketing and
904
904
 
905
905
  **Tech stack:** TypeScript, Node.js, esbuild, MCP SDK, Zod, GHL API v2, Firebase Auth
906
906
 
907
- **Version:** 3.95.0
907
+ **Version:** 3.96.0
908
908
 
909
909
  ---
910
910
 
package/dist/index.js CHANGED
@@ -6320,6 +6320,43 @@ var init_contacts = __esm({
6320
6320
  });
6321
6321
 
6322
6322
  // src/tools/conversations.ts
6323
+ async function assertNumberOnAccount(client, fromNumber) {
6324
+ const locationId2 = client.resolveLocationId(void 0);
6325
+ let numbers;
6326
+ try {
6327
+ const raw = await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } });
6328
+ numbers = NumbersResponseSchema.parse(raw).phoneNumbers.map((n) => n.value);
6329
+ } catch (e) {
6330
+ const why = e instanceof Error ? e.message : String(e);
6331
+ throw new Error(
6332
+ `SEND_NOT_SENT: could not confirm ${fromNumber} is a number on sub-account ${locationId2} (${why}). Nothing was sent.`
6333
+ );
6334
+ }
6335
+ if (!numbers.some((n) => sameLine(n, fromNumber))) {
6336
+ throw new Error(
6337
+ `SEND_NOT_SENT: ${fromNumber} is not a phone number on sub-account ${locationId2}. Numbers on this account: ${numbers.length ? numbers.join(", ") : "(none)"}. Nothing was sent.`
6338
+ );
6339
+ }
6340
+ }
6341
+ function isKnownPreSendRefusal(e) {
6342
+ if (!(e instanceof Error)) return false;
6343
+ if (e.name === "PacerCeilingError") return true;
6344
+ return e.message.startsWith("Refused:");
6345
+ }
6346
+ function classifySendFailure(e) {
6347
+ const why = e instanceof Error ? e.message : String(e);
6348
+ if (!isGHLRequestError(e) && isKnownPreSendRefusal(e)) {
6349
+ return new Error(`SEND_NOT_SENT: refused before sending (${why}). Nothing was sent.`);
6350
+ }
6351
+ const m = /GHL API Error (\d{3})/.exec(why);
6352
+ const status = m ? Number(m[1]) : null;
6353
+ if (status !== null && status >= 400 && status < 500 && status !== 408) {
6354
+ return new Error(`SEND_NOT_SENT: GHL refused the message (HTTP ${status}). Nothing was sent. ${why}`);
6355
+ }
6356
+ return new Error(
6357
+ `SEND_OUTCOME_UNKNOWN: the send attempt failed in a way that does not say whether GHL accepted it (${why}). It may have gone out. Not retried. Check the contact's conversation (search_conversations, get_messages) before sending again.`
6358
+ );
6359
+ }
6323
6360
  function registerConversationTools(server2, client) {
6324
6361
  safeTool(
6325
6362
  server2,
@@ -6410,9 +6447,25 @@ function registerConversationTools(server2, client) {
6410
6447
  emailTo: import_zod10.z.string().optional().describe("Recipient email address (for Email type)."),
6411
6448
  emailCc: import_zod10.z.array(import_zod10.z.string()).optional().describe("CC email addresses (for Email type)."),
6412
6449
  emailBcc: import_zod10.z.array(import_zod10.z.string()).optional().describe("BCC email addresses (for Email type)."),
6413
- attachments: import_zod10.z.array(import_zod10.z.string()).optional().describe("Array of attachment URLs to include with the message.")
6450
+ attachments: import_zod10.z.array(import_zod10.z.string()).optional().describe("Array of attachment URLs to include with the message."),
6451
+ fromNumber: import_zod10.z.string().regex(E164, "fromNumber must be E.164, e.g. +16234007574").optional().describe(
6452
+ `SMS only: which of the sub-account's own phone numbers to send FROM, E.164 (e.g. "+16234007574"). Checked against the account's provisioned numbers (list_phone_numbers) BEFORE sending; a number that is not on the account, or a check that cannot run, refuses the send and nothing goes out. After sending, the number GHL actually used is read back and compared: see sender_check. Omit to let GHL pick its default line.`
6453
+ ),
6454
+ noRetry: import_zod10.z.boolean().optional().describe(
6455
+ "true = send exactly once: no automatic retry after a network error, timeout, rate limit or GHL server error. Use for unattended senders, where a retry after a lost-but-successful response would text the person twice. A failed attempt is then reported as SEND_OUTCOME_UNKNOWN (it may have gone out: check the conversation before sending again) or SEND_NOT_SENT (GHL refused it). Default false keeps the existing retries."
6456
+ )
6414
6457
  },
6415
- async ({ type, contactId, message, subject, html: html2, emailFrom, emailTo, emailCc, emailBcc, attachments }) => {
6458
+ async ({ type, contactId, message, subject, html: html2, emailFrom, emailTo, emailCc, emailBcc, attachments, fromNumber, noRetry }) => {
6459
+ const c = client.pinned();
6460
+ if (fromNumber !== void 0) {
6461
+ if (type !== "SMS") {
6462
+ throw new Error(`SEND_NOT_SENT: fromNumber applies to SMS only (type was ${type}). Nothing was sent.`);
6463
+ }
6464
+ if (!E164.test(fromNumber)) {
6465
+ throw new Error(`SEND_NOT_SENT: fromNumber must be E.164, e.g. +16234007574 (got "${fromNumber}"). Nothing was sent.`);
6466
+ }
6467
+ await assertNumberOnAccount(c, fromNumber);
6468
+ }
6416
6469
  const body = { type, contactId };
6417
6470
  if (message !== void 0) body.message = message;
6418
6471
  if (subject !== void 0) body.subject = subject;
@@ -6422,9 +6475,25 @@ function registerConversationTools(server2, client) {
6422
6475
  if (emailCc !== void 0) body.emailCc = emailCc;
6423
6476
  if (emailBcc !== void 0) body.emailBcc = emailBcc;
6424
6477
  if (attachments !== void 0) body.attachments = attachments;
6425
- const result = await client.post("/conversations/messages", { body });
6478
+ if (fromNumber !== void 0) body.fromNumber = fromNumber;
6479
+ let result;
6480
+ try {
6481
+ result = await c.post("/conversations/messages", noRetry ? { body, noRetry: true } : { body });
6482
+ } catch (e) {
6483
+ if (!noRetry) throw e;
6484
+ throw classifySendFailure(e);
6485
+ }
6426
6486
  const messageId = isRecord4(result) && typeof result.messageId === "string" ? result.messageId : null;
6427
- if (!messageId) return result;
6487
+ if (!messageId) {
6488
+ if (fromNumber === void 0) return result;
6489
+ const sender_check2 = {
6490
+ requested: fromNumber,
6491
+ actual: null,
6492
+ matches: false,
6493
+ warning: `SENDER NOT VERIFIED. GHL returned no messageId, so neither delivery nor the line used (${fromNumber} requested) could be read back.`
6494
+ };
6495
+ return isRecord4(result) ? { ...result, sender_check: sender_check2 } : { result, sender_check: sender_check2 };
6496
+ }
6428
6497
  const TERMINAL = /* @__PURE__ */ new Set(["delivered", "sent", "failed", "undelivered", "rejected", "opted_out"]);
6429
6498
  let status = null;
6430
6499
  let from = null;
@@ -6432,7 +6501,7 @@ function registerConversationTools(server2, client) {
6432
6501
  for (let attempt2 = 0; attempt2 < 3; attempt2++) {
6433
6502
  if (attempt2 > 0) await new Promise((r) => setTimeout(r, 1200 * attempt2));
6434
6503
  try {
6435
- const raw = await client.get(`/conversations/messages/${messageId}`);
6504
+ const raw = await c.get(`/conversations/messages/${messageId}`);
6436
6505
  const msg3 = isRecord4(raw) && isRecord4(raw.message) ? raw.message : isRecord4(raw) ? raw : null;
6437
6506
  status = typeof msg3?.status === "string" ? msg3.status : null;
6438
6507
  from = typeof msg3?.from === "string" ? msg3.from : from;
@@ -6454,7 +6523,35 @@ function registerConversationTools(server2, client) {
6454
6523
  } else if (!delivered) {
6455
6524
  delivery_check.warning = `NOT DELIVERED. GHL reports status "${status ?? "(none)"}" for this message. Do not report it as sent.`;
6456
6525
  }
6457
- return isRecord4(result) ? { ...result, delivery_check } : { result, delivery_check };
6526
+ let sender_check;
6527
+ if (fromNumber !== void 0) {
6528
+ if (from === null) {
6529
+ sender_check = {
6530
+ requested: fromNumber,
6531
+ actual: null,
6532
+ matches: false,
6533
+ warning: `SENDER NOT VERIFIED. GHL's readback did not say which number this went out from, so it is unknown whether ${fromNumber} was used.`
6534
+ };
6535
+ } else if (!isPhoneLike(from)) {
6536
+ sender_check = {
6537
+ requested: fromNumber,
6538
+ actual: from,
6539
+ matches: false,
6540
+ warning: `SENDER NOT VERIFIED. GHL's readback names the sender as "${from}", which is a label, not a phone number, so it is unknown whether ${fromNumber} was used. Check the message in the conversation (get_message) before reporting which line sent it.`
6541
+ };
6542
+ } else if (sameLine(from, fromNumber)) {
6543
+ sender_check = { requested: fromNumber, actual: from, matches: true };
6544
+ } else {
6545
+ sender_check = {
6546
+ requested: fromNumber,
6547
+ actual: from,
6548
+ matches: false,
6549
+ warning: `WRONG SENDER. You asked for ${fromNumber}; GHL sent from ${from}. The message still went out (see delivery_check). Do not report it as sent from ${fromNumber}.`
6550
+ };
6551
+ }
6552
+ }
6553
+ const extra = sender_check ? { delivery_check, sender_check } : { delivery_check };
6554
+ return isRecord4(result) ? { ...result, ...extra } : { result, ...extra };
6458
6555
  }
6459
6556
  );
6460
6557
  safeTool(
@@ -6500,13 +6597,24 @@ function registerConversationTools(server2, client) {
6500
6597
  }
6501
6598
  );
6502
6599
  }
6503
- var import_zod10, isRecord4;
6600
+ var import_zod10, isRecord4, E164, digits, sameLine, isPhoneLike, NumbersResponseSchema;
6504
6601
  var init_conversations = __esm({
6505
6602
  "src/tools/conversations.ts"() {
6506
6603
  "use strict";
6507
6604
  import_zod10 = require("zod");
6605
+ init_ghl_client();
6508
6606
  init_tool_helpers();
6509
6607
  isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6608
+ E164 = /^\+[1-9]\d{6,14}$/;
6609
+ digits = (s) => s.replace(/\D/g, "");
6610
+ sameLine = (a, b) => {
6611
+ const da = digits(a), db = digits(b);
6612
+ if (da === db) return true;
6613
+ const [shortRaw, short, long] = da.length < db.length ? [a, da, db] : [b, db, da];
6614
+ return !shortRaw.trim().startsWith("+") && short.length === 10 && long.length === 11 && long === "1" + short;
6615
+ };
6616
+ isPhoneLike = (s) => /^[+\d\s().-]+$/.test(s.trim()) && digits(s).length >= 7;
6617
+ NumbersResponseSchema = import_zod10.z.object({ phoneNumbers: import_zod10.z.array(import_zod10.z.object({ value: import_zod10.z.string() }).passthrough()) }).passthrough();
6510
6618
  }
6511
6619
  });
6512
6620
 
@@ -8424,12 +8532,12 @@ function buildCreateUserBody(args, opts = {}) {
8424
8532
  function generateTemporaryPassword() {
8425
8533
  const upper2 = "ABCDEFGHJKLMNPQRSTUVWXYZ";
8426
8534
  const lower = "abcdefghijkmnopqrstuvwxyz";
8427
- const digits2 = "23456789";
8535
+ const digits3 = "23456789";
8428
8536
  const symbols = "!@#$%&*?";
8429
- const all = upper2 + lower + digits2 + symbols;
8537
+ const all = upper2 + lower + digits3 + symbols;
8430
8538
  const bytes = (0, import_node_crypto4.randomBytes)(24);
8431
8539
  const pick3 = (set, b) => set[b % set.length];
8432
- const chars = [pick3(upper2, bytes[0]), pick3(lower, bytes[1]), pick3(digits2, bytes[2]), pick3(symbols, bytes[3])];
8540
+ const chars = [pick3(upper2, bytes[0]), pick3(lower, bytes[1]), pick3(digits3, bytes[2]), pick3(symbols, bytes[3])];
8433
8541
  for (let i = 4; i < 20; i++) chars.push(pick3(all, bytes[i]));
8434
8542
  for (let i = chars.length - 1; i > 0; i--) {
8435
8543
  const j = bytes[(i + 20) % bytes.length] % (i + 1);
@@ -11411,7 +11519,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
11411
11519
  const numbers = await phoneNumbers(client, loc);
11412
11520
  let demoNumber;
11413
11521
  if (demoPhoneNumber !== void 0) {
11414
- if (!E164.test(demoPhoneNumber)) throw new Error(`demoPhoneNumber must be E.164, e.g. +16234007574 (got "${demoPhoneNumber}").`);
11522
+ if (!E1642.test(demoPhoneNumber)) throw new Error(`demoPhoneNumber must be E.164, e.g. +16234007574 (got "${demoPhoneNumber}").`);
11415
11523
  if (!numbers.includes(demoPhoneNumber)) throw new Error(`${demoPhoneNumber} is not a number on this sub-account. Its numbers: ${numbers.join(", ") || "none"}.`);
11416
11524
  demoNumber = demoPhoneNumber;
11417
11525
  } else if (numbers.length === 1) demoNumber = numbers[0];
@@ -11483,7 +11591,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
11483
11591
  demoLocationId: import_zod36.z.string().describe("The demo sub-account id. Must equal the active location."),
11484
11592
  website: import_zod36.z.string().url().describe("The prospect's website, e.g. https://northsideclinic.com"),
11485
11593
  businessName: import_zod36.z.string().min(1).describe("The prospect's business, as the bot should say it."),
11486
- prospectPhone: import_zod36.z.string().regex(E164, "E.164, e.g. +16235551234").describe("The phone the prospect will text FROM, in E.164 (+16235551234). The demo is bound to this one contact."),
11594
+ prospectPhone: import_zod36.z.string().regex(E1642, "E.164, e.g. +16235551234").describe("The phone the prospect will text FROM, in E.164 (+16235551234). The demo is bound to this one contact."),
11487
11595
  useExistingContact: import_zod36.z.boolean().optional().describe("If a contact with this phone already exists and is not a demo prospect, say so explicitly to write the demo fields onto it. Default: refuse, and name the contact."),
11488
11596
  prospectName: import_zod36.z.string().optional().describe("The prospect's name, for the contact record."),
11489
11597
  industry: import_zod36.z.string().optional().describe("e.g. 'med spa', 'chiropractic'. Default: 'healthcare'."),
@@ -11747,7 +11855,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
11747
11855
  }
11748
11856
  );
11749
11857
  }
11750
- var import_zod36, import_node_crypto5, DEMO_FIELDS, DEMO_BOT_NAME, DEMO_BOT_PROMPT, DEFAULT_QUESTIONS, DEMO_PROSPECT_TAG, E164, PRIMARY_NOTE, AI;
11858
+ var import_zod36, import_node_crypto5, DEMO_FIELDS, DEMO_BOT_NAME, DEMO_BOT_PROMPT, DEFAULT_QUESTIONS, DEMO_PROSPECT_TAG, E1642, PRIMARY_NOTE, AI;
11751
11859
  var init_ai_employee_demo = __esm({
11752
11860
  "src/tools/ai-employee-demo.ts"() {
11753
11861
  "use strict";
@@ -11782,7 +11890,7 @@ var init_ai_employee_demo = __esm({
11782
11890
  };
11783
11891
  DEFAULT_QUESTIONS = ["Which service are you most interested in today?", "What result are you hoping to achieve?", "Is there anything we should know before booking?"];
11784
11892
  DEMO_PROSPECT_TAG = "ai-employee-demo-prospect";
11785
- E164 = /^\+[1-9]\d{6,14}$/;
11893
+ E1642 = /^\+[1-9]\d{6,14}$/;
11786
11894
  PRIMARY_NOTE = "Which bot answers SMS is set in GoHighLevel's screen (Settings \u2192 Conversation AI), not by API. Until the demo bot is primary on SMS there, texts to the number are answered by whichever bot is, or by nobody.";
11787
11895
  AI = { version: AI_API_VERSION };
11788
11896
  }
@@ -20355,8 +20463,8 @@ function decodeEntitiesAs(s, exact) {
20355
20463
  const NAMED2 = { nbsp: " ", amp: "&", apos: "'", quot: '"', lt: "<", gt: ">", rsquo: "'", lsquo: "'", rdquo: '"', ldquo: '"', ndash: "-", mdash: "-", hellip: "\u2026", copy: "\xA9", reg: "\xAE", trade: "\u2122" };
20356
20464
  return s.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z][a-z0-9]{1,15}));?/gi, (m, dec, hex, name2) => {
20357
20465
  if (dec || hex) {
20358
- const digits2 = (dec ?? hex).replace(/^0+(?=.)/, "");
20359
- const cp = digits2.length > 8 ? Infinity : dec ? parseInt(digits2, 10) : parseInt(digits2, 16);
20466
+ const digits3 = (dec ?? hex).replace(/^0+(?=.)/, "");
20467
+ const cp = digits3.length > 8 ? Infinity : dec ? parseInt(digits3, 10) : parseInt(digits3, 16);
20360
20468
  if (!cp || cp > 1114111 || cp >= 55296 && cp <= 57343) return "\uFFFD";
20361
20469
  const c = String.fromCodePoint(cp);
20362
20470
  if (exact) return c === "\xA0" ? " " : c;
@@ -47688,7 +47796,7 @@ var require_package = __commonJS({
47688
47796
  "package.json"(exports2, module2) {
47689
47797
  module2.exports = {
47690
47798
  name: "@elitedcs/ghl-mcp",
47691
- version: "3.95.0",
47799
+ version: "3.96.0",
47692
47800
  mcpName: "io.github.drjerryrelth/ghl-command",
47693
47801
  description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
47694
47802
  main: "dist/index.js",
@@ -58965,7 +59073,7 @@ function readAllowList(env2 = process.env) {
58965
59073
  const e = normalEmail(entry);
58966
59074
  const p = normalPhone(entry);
58967
59075
  if (EMAIL2.test(e)) emails.add(e);
58968
- else if (E1642.test(p)) phones.add(p);
59076
+ else if (E1643.test(p)) phones.add(p);
58969
59077
  else return { ok: false, why: `The sandbox allow-list (${SANDBOX_SEND_ALLOW_ENV}) has an entry that is not one exact email or one +phone number: "${entry.slice(0, 60)}". Nothing is sent until it is fixed.` };
58970
59078
  }
58971
59079
  return { ok: true, emails, phones };
@@ -59011,14 +59119,14 @@ function leadRecipient(channel, r) {
59011
59119
  if (!/^\+?\d{7,15}$/.test(phone)) return { ok: false, rail: "recipient", why: "This lead has no usable phone number in GoHighLevel, so nothing was sent." };
59012
59120
  return { ok: true, to: phone };
59013
59121
  }
59014
- var SANDBOX_SEND_ENV, SANDBOX_SEND_ALLOW_ENV, EMAIL2, E1642, normalEmail, normalPhone, SWITCH_OFF_WHY;
59122
+ var SANDBOX_SEND_ENV, SANDBOX_SEND_ALLOW_ENV, EMAIL2, E1643, normalEmail, normalPhone, SWITCH_OFF_WHY;
59015
59123
  var init_send_switch = __esm({
59016
59124
  "src/command-os/brain/send-switch.ts"() {
59017
59125
  "use strict";
59018
59126
  SANDBOX_SEND_ENV = "BRAIN_SANDBOX_SEND";
59019
59127
  SANDBOX_SEND_ALLOW_ENV = "BRAIN_SANDBOX_SEND_ALLOW";
59020
59128
  EMAIL2 = /^[^\s@,*]+@[^\s@,*]+\.[a-z]{2,}$/;
59021
- E1642 = /^\+[1-9]\d{7,14}$/;
59129
+ E1643 = /^\+[1-9]\d{7,14}$/;
59022
59130
  normalEmail = (s) => s.trim().toLowerCase();
59023
59131
  normalPhone = (s) => s.trim().replace(/[\s().-]/g, "");
59024
59132
  SWITCH_OFF_WHY = `Sandbox sending is switched off, so nothing was sent. It is on only for a command run with ${SANDBOX_SEND_ENV}=on, and only ever to a sandbox test address.`;
@@ -61318,8 +61426,8 @@ function maskRecipient(to) {
61318
61426
  if (!to) return null;
61319
61427
  const at = to.indexOf("@");
61320
61428
  if (at > 0) return `${to.slice(0, Math.min(2, at))}***${to.slice(at)}`;
61321
- const digits2 = to.replace(/\D/g, "");
61322
- return digits2.length >= 4 ? `***${digits2.slice(-4)}` : "***";
61429
+ const digits3 = to.replace(/\D/g, "");
61430
+ return digits3.length >= 4 ? `***${digits3.slice(-4)}` : "***";
61323
61431
  }
61324
61432
  function sendLogView(log, locationId2, limit = 200) {
61325
61433
  const entries = log.entries();
@@ -63349,9 +63457,9 @@ function handoffInAccount(h, facts, readback, plannedUsers = [], openRecipientJo
63349
63457
  }
63350
63458
  if (!READ_HANDOFFS.has(h.ref)) return { readable: false, done: false };
63351
63459
  if (h.ref === "handoff.phone_number") {
63352
- const phones = (facts.phoneValues ?? []).map(digits).filter((d) => d.length === 10);
63460
+ const phones = (facts.phoneValues ?? []).map(digits2).filter((d) => d.length === 10);
63353
63461
  const biz = valueOf(facts, "business_phone");
63354
- return { readable: true, done: phones.length > 0 && biz !== void 0 && digits(biz).length === 10 };
63462
+ return { readable: true, done: phones.length > 0 && biz !== void 0 && digits2(biz).length === 10 };
63355
63463
  }
63356
63464
  if (h.ref === "handoff.add_staff") {
63357
63465
  const users = facts.userList;
@@ -63366,7 +63474,7 @@ function handoffInAccount(h, facts, readback, plannedUsers = [], openRecipientJo
63366
63474
  const vals = keys.map((k) => valueOf(facts, k));
63367
63475
  if (!keys.length || vals.some((v) => v === void 0)) return { readable: true, done: false };
63368
63476
  const at = (k) => vals[keys.indexOf(k)];
63369
- if (h.ref === "handoff.business_details") return { readable: true, done: !keys.includes("business_phone") || digits(at("business_phone")).length === 10 };
63477
+ if (h.ref === "handoff.business_details") return { readable: true, done: !keys.includes("business_phone") || digits2(at("business_phone")).length === 10 };
63370
63478
  if (!vals.every(httpsUrl2)) return { readable: true, done: false };
63371
63479
  if (h.ref === "handoff.link_domain") return { readable: true, done: bookingLinkInAccount(vals, facts) === true };
63372
63480
  return { readable: true, done: true };
@@ -63473,7 +63581,7 @@ function handoffShownText(h, facts) {
63473
63581
  const instruction = h.instruction || "Finish this manual action.";
63474
63582
  if (h.ref === "handoff.business_details") return obligationTaskText({ type: "handoff", name: title2.replace(/^Confirm your\b/, "Set your"), reason: instruction });
63475
63583
  if (h.ref !== "handoff.phone_number") return handoffJobText(h);
63476
- const numbers = facts?.phoneValues?.filter((p) => digits(p).length === 10);
63584
+ const numbers = facts?.phoneValues?.filter((p) => digits2(p).length === 10);
63477
63585
  if (numbers && !numbers.length) return handoffJobText(h);
63478
63586
  const rest = instruction.replace(/^[^]*?\bbuy\b[^]*?\.(?:\s+|$)/i, "");
63479
63587
  const tail = rest === instruction ? "" : rest.replace(/\bA2P registration \(next\) needs the number too\b/, "A2P registration needs the number too");
@@ -63557,10 +63665,10 @@ function timingOf(texts) {
63557
63665
  return "later";
63558
63666
  }
63559
63667
  function phoneInfo(facts) {
63560
- const phones = (facts.phoneValues ?? []).filter((p) => digits(p).length === 10);
63668
+ const phones = (facts.phoneValues ?? []).filter((p) => digits2(p).length === 10);
63561
63669
  const biz = valueOf(facts, "business_phone");
63562
- if (!phones.length || biz === void 0 || digits(biz).length !== 10) return void 0;
63563
- if (phones.some((p) => digits(p) === digits(biz))) return void 0;
63670
+ if (!phones.length || biz === void 0 || digits2(biz).length !== 10) return void 0;
63671
+ if (phones.some((p) => digits2(p) === digits2(biz))) return void 0;
63564
63672
  return `Your texts come from ${phones[0]}; your business line is ${biz}. That is fine if the business line is the clinic's own number.`;
63565
63673
  }
63566
63674
  function fitHandoff(h, all, facts) {
@@ -63715,7 +63823,7 @@ function handoffPartId(ref) {
63715
63823
  const part = HANDOFF_PARTS[ref];
63716
63824
  return part ? `${ref}:${part.part}` : void 0;
63717
63825
  }
63718
- var key, digits, httpsUrl2, GHL_WIDGET_HOST, money6, placeholder, field, READ_HANDOFFS, HANDOFF_PARTS, ASSIGN_CALENDAR_STAFF, READ_KINDS, NOT_A_JOB, publishedRead;
63826
+ var key, digits2, httpsUrl2, GHL_WIDGET_HOST, money6, placeholder, field, READ_HANDOFFS, HANDOFF_PARTS, ASSIGN_CALENDAR_STAFF, READ_KINDS, NOT_A_JOB, publishedRead;
63719
63827
  var init_jobs = __esm({
63720
63828
  "src/jobs.ts"() {
63721
63829
  "use strict";
@@ -63724,7 +63832,7 @@ var init_jobs = __esm({
63724
63832
  init_account_reads();
63725
63833
  init_executor();
63726
63834
  key = (name2) => name2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
63727
- digits = (v) => String(v ?? "").replace(/\D/g, "").slice(-10);
63835
+ digits2 = (v) => String(v ?? "").replace(/\D/g, "").slice(-10);
63728
63836
  httpsUrl2 = (v) => {
63729
63837
  try {
63730
63838
  const u = new URL(String(v ?? "").trim());
@@ -86969,20 +87077,20 @@ function checkConversion(p, ctx, rendered) {
86969
87077
  invented.push(`quote attributed to ${t.name} that they did not give`);
86970
87078
  }
86971
87079
  }
86972
- const digits2 = (x) => x.replace(/[,\s]/g, "");
87080
+ const digits3 = (x) => x.replace(/[,\s]/g, "");
86973
87081
  const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "our", "over", "of", "in", "on", "a", "an", "we", "us", "have", "been"]);
86974
87082
  const unitAfter = (text3, at) => norm6(text3.slice(at)).split(" ").slice(1).filter((w) => w && !STOP.has(w) && !/^[\d,.]+$/.test(w)).slice(0, 2);
86975
87083
  const briefFigures = b.proof.aggregates.flatMap((agg) => {
86976
87084
  const line4 = `${agg.value} ${agg.label}`;
86977
- return [...line4.matchAll(/\d[\d,.]*/g)].map((m) => ({ n: digits2(m[0]), units: /* @__PURE__ */ new Set([...unitAfter(line4, m.index ?? 0), ...norm6(agg.label).split(" ").filter((w) => w && !STOP.has(w))]) }));
87085
+ return [...line4.matchAll(/\d[\d,.]*/g)].map((m) => ({ n: digits3(m[0]), units: /* @__PURE__ */ new Set([...unitAfter(line4, m.index ?? 0), ...norm6(agg.label).split(" ").filter((w) => w && !STOP.has(w))]) }));
86978
87086
  });
86979
87087
  const seenClaims = /* @__PURE__ */ new Set();
86980
87088
  const claimed = (text3) => {
86981
87089
  for (const m of text3.matchAll(/\d[\d,.]*/g)) {
86982
- const key2 = `${digits2(m[0])}@${norm6(text3).slice(0, 40)}`;
87090
+ const key2 = `${digits3(m[0])}@${norm6(text3).slice(0, 40)}`;
86983
87091
  if (seenClaims.has(key2)) continue;
86984
87092
  seenClaims.add(key2);
86985
- const hit = briefFigures.find((f) => f.n === digits2(m[0]));
87093
+ const hit = briefFigures.find((f) => f.n === digits3(m[0]));
86986
87094
  if (!hit) {
86987
87095
  invented.push(`figure "${m[0]}" in "${text3.slice(0, 30)}"`);
86988
87096
  continue;
@@ -90959,9 +91067,9 @@ function looksLikeRawId(word) {
90959
91067
  if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(word)) return true;
90960
91068
  if (/^[0-9a-f]{24}$/.test(word)) return true;
90961
91069
  if (!/^[A-Za-z0-9]{17,}$/.test(word)) return false;
90962
- const digits2 = (word.match(/\d/g) ?? []).length;
91070
+ const digits3 = (word.match(/\d/g) ?? []).length;
90963
91071
  const letters = (word.match(/[A-Za-z]/g) ?? []).length;
90964
- if (digits2 < 2 || letters < 2) return false;
91072
+ if (digits3 < 2 || letters < 2) return false;
90965
91073
  const longestLetterRun = Math.max(0, ...(word.match(/[a-z]+|[A-Z]+/g) ?? []).map((r) => r.length));
90966
91074
  return longestLetterRun <= 3;
90967
91075
  }
@@ -92893,7 +93001,7 @@ function registerPhoneTools(server2, client) {
92893
93001
  async ({ locationId: locationId2 }) => {
92894
93002
  const loc = client.resolveLocationId(locationId2);
92895
93003
  const raw = await client.get("/phone-system/numbers/", { params: { locationId: loc } });
92896
- const parsed2 = NumbersResponseSchema.parse(raw);
93004
+ const parsed2 = NumbersResponseSchema2.parse(raw);
92897
93005
  return {
92898
93006
  locationId: loc,
92899
93007
  count: parsed2.phoneNumbers.length,
@@ -92916,14 +93024,14 @@ function registerPhoneTools(server2, client) {
92916
93024
  }
92917
93025
  );
92918
93026
  }
92919
- var import_zod119, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
93027
+ var import_zod119, PhoneNumberSchema, NumbersResponseSchema2, PoolsResponseSchema;
92920
93028
  var init_phone = __esm({
92921
93029
  "src/tools/phone.ts"() {
92922
93030
  "use strict";
92923
93031
  import_zod119 = require("zod");
92924
93032
  init_tool_helpers();
92925
93033
  PhoneNumberSchema = import_zod119.z.object({ sid: import_zod119.z.string(), value: import_zod119.z.string(), title: import_zod119.z.string().optional() }).passthrough();
92926
- NumbersResponseSchema = import_zod119.z.object({ phoneNumbers: import_zod119.z.array(PhoneNumberSchema) }).passthrough();
93034
+ NumbersResponseSchema2 = import_zod119.z.object({ phoneNumbers: import_zod119.z.array(PhoneNumberSchema) }).passthrough();
92927
93035
  PoolsResponseSchema = import_zod119.z.object({ pools: import_zod119.z.array(import_zod119.z.object({}).passthrough()) }).passthrough();
92928
93036
  }
92929
93037
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.95.0",
3
+ "version": "3.96.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
6
6
  "main": "dist/index.js",