@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 +12 -0
- package/README.md +1 -1
- package/dist/index.js +148 -40
- package/package.json +1 -1
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
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
8535
|
+
const digits3 = "23456789";
|
|
8428
8536
|
const symbols = "!@#$%&*?";
|
|
8429
|
-
const all = upper2 + lower +
|
|
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(
|
|
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 (!
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
|
20359
|
-
const cp =
|
|
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.
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
61322
|
-
return
|
|
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(
|
|
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 &&
|
|
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") ||
|
|
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) =>
|
|
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) =>
|
|
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 ||
|
|
63563
|
-
if (phones.some((p) =>
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 = `${
|
|
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 ===
|
|
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
|
|
91070
|
+
const digits3 = (word.match(/\d/g) ?? []).length;
|
|
90963
91071
|
const letters = (word.match(/[A-Za-z]/g) ?? []).length;
|
|
90964
|
-
if (
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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",
|